From baccb26f1aecd27b7d7fc06a185e7fdeaf03ea83 Mon Sep 17 00:00:00 2001 From: Simon Brandhof Date: Tue, 5 Dec 2017 18:04:06 +0100 Subject: [PATCH] SONAR-10117 SONAR-1018 Update measures after relevant issue changes and send webhooks --- .../xoo/rule/OneBugIssuePerLineSensor.java | 10 +- .../container/ComputeEngineContainerImpl.java | 3 + .../ComputeEngineContainerImplTest.java | 3 +- .../org/sonar/db/component/ComponentDto.java | 8 +- .../java/org/sonar/db/issue/IssueDao.java | 4 + .../org/sonar/db/issue/IssueGroupDto.java | 98 +++ .../java/org/sonar/db/issue/IssueMapper.java | 6 + .../org/sonar/db/measure/LiveMeasureDao.java | 5 +- .../java/org/sonar/db/metric/MetricDao.java | 2 +- .../sonar/db/webhook/WebhookDeliveryDto.java | 3 +- .../org/sonar/db/issue/IssueMapper.xml | 36 + .../sonar/db/component/ComponentDtoTest.java | 25 +- .../java/org/sonar/db/issue/IssueDaoTest.java | 55 ++ .../sonar/db/measure/LiveMeasureDaoTest.java | 148 +++- .../java/org/sonar/db/purge/PurgeDaoTest.java | 4 +- .../component/index/ComponentIndexer.java | 3 +- .../sonar/server/component/ws/AppAction.java | 2 +- .../PostProjectAnalysisTasksExecutor.java | 2 +- .../formula/counter/RatingValue.java | 2 +- .../qualitygate/MutableQualityGateHolder.java | 12 +- .../qualitygate/QualityGateHolder.java | 10 +- .../qualitygate/QualityGateHolderImpl.java | 43 +- .../qualitygate/QualityGateService.java | 5 +- .../qualitygate/QualityGateServiceImpl.java | 6 +- .../{RatingGrid.java => DebtRatingGrid.java} | 52 +- .../MaintainabilityMeasuresVisitor.java | 7 +- .../NewMaintainabilityMeasuresVisitor.java | 11 +- ...ilityAndSecurityRatingMeasuresVisitor.java | 12 +- .../projectanalysis/qualitymodel/Rating.java | 63 ++ .../qualitymodel/RatingSettings.java | 67 +- ...ilityAndSecurityRatingMeasuresVisitor.java | 78 +- .../step/LoadQualityGateStep.java | 10 +- .../step/QualityGateMeasuresStep.java | 4 +- .../webhook/WebhookPostTask.java | 3 +- .../org/sonar/server/es/ProjectIndexer.java | 3 +- .../issue/AbstractChangeTagsAction.java | 5 + .../java/org/sonar/server/issue/Action.java | 2 + .../org/sonar/server/issue/AssignAction.java | 5 + .../org/sonar/server/issue/CommentAction.java | 5 + .../issue/IssueChangePostProcessor.java | 39 + .../issue/IssueChangePostProcessorImpl.java | 46 + .../org/sonar/server/issue/IssueUpdater.java | 40 +- .../sonar/server/issue/SetSeverityAction.java | 5 + .../org/sonar/server/issue/SetTypeAction.java | 5 + .../sonar/server/issue/TransitionAction.java | 5 + .../server/issue/index/IssueIndexer.java | 3 +- .../server/issue/ws/AddCommentAction.java | 2 +- .../sonar/server/issue/ws/AssignAction.java | 4 +- .../server/issue/ws/BulkChangeAction.java | 112 +-- .../server/issue/ws/DoTransitionAction.java | 20 +- .../sonar/server/issue/ws/IssueWsModule.java | 4 - .../server/issue/ws/SetSeverityAction.java | 2 +- .../sonar/server/issue/ws/SetTagsAction.java | 2 +- .../sonar/server/issue/ws/SetTypeAction.java | 21 +- .../measure/index/ProjectMeasuresIndexer.java | 17 +- .../server/measure/live/IssueCounter.java | 153 ++++ .../measure/live/IssueMetricFormula.java | 89 ++ .../live/IssueMetricFormulaFactory.java | 40 + .../live/IssueMetricFormulaFactoryImpl.java | 200 +++++ .../measure/live/LiveMeasureComputer.java | 43 + .../measure/live/LiveMeasureComputerImpl.java | 242 +++++ .../measure/live/LiveMeasureModule.java | 32 + .../measure/live/LiveQualityGateComputer.java | 40 + .../live/LiveQualityGateComputerImpl.java | 151 ++++ .../server/measure/live/MeasureMatrix.java | 203 +++++ .../server/measure/live/package-info.java | 23 + .../server/measure/ws/ComponentAction.java | 2 +- .../sonar/server/measure/ws/SearchAction.java | 2 +- .../permission/index/PermissionIndexer.java | 4 +- .../platformlevel/PlatformLevel4.java | 4 + .../sonar/server/qualitygate/Condition.java | 19 +- .../qualitygate/ConditionEvaluator.java | 181 ++++ .../qualitygate/EvaluatedCondition.java | 3 +- .../qualitygate/EvaluatedQualityGate.java | 56 +- .../LiveQualityGateFactoryImpl.java | 134 --- .../QualityGateConditionsUpdater.java | 4 +- .../qualitygate/QualityGateConverter.java | 61 ++ .../qualitygate/QualityGateEvaluator.java | 60 ++ .../qualitygate/QualityGateEvaluatorImpl.java | 133 +++ .../server/qualitygate/QualityGateFinder.java | 12 +- .../server/qualitygate/QualityGateModule.java | 1 + .../qualitygate/RegisterQualityGates.java | 4 +- .../ShortLivingBranchQualityGate.java | 6 + .../changeevent/QGChangeEventFactory.java | 138 --- .../changeevent/QGChangeEventFactoryImpl.java | 156 ---- .../changeevent/QGChangeEventListeners.java | 4 +- .../QGChangeEventListenersImpl.java | 7 +- .../qualitygate/ws/GetByProjectAction.java | 2 +- .../settings/ProjectConfigurationLoader.java | 9 + .../sonar/server/test/index/TestIndexer.java | 5 +- .../sonar/server/ui/ws/ComponentAction.java | 2 +- .../webhook/WebhookQGChangeEventListener.java | 11 +- .../server/webhook/ws/WebhookWsSupport.java | 15 +- .../formula/counter/RatingValueTest.java | 10 +- .../measure/BestValueOptimizationTest.java | 4 +- .../MutableQualityGateHolderRule.java | 16 +- .../QualityGateHolderImplTest.java | 33 +- .../qualitygate/QualityGateHolderRule.java | 19 +- ...gGridTest.java => DebtRatingGridTest.java} | 18 +- .../MaintainabilityMeasuresVisitorTest.java | 9 +- ...NewMaintainabilityMeasuresVisitorTest.java | 54 +- ...yAndSecurityRatingMeasuresVisitorTest.java | 12 +- .../qualitymodel/RatingSettingsTest.java | 24 +- ...yAndSecurityRatingMeasuresVisitorTest.java | 12 +- .../step/LoadQualityGateStepTest.java | 28 +- .../webhook/WebhookPostTaskTest.java | 3 +- .../org/sonar/server/issue/ActionTest.java | 61 +- .../sonar/server/issue/IssueUpdaterTest.java | 16 +- .../issue/TestIssueChangePostProcessor.java | 47 + .../server/issue/ws/AddCommentActionTest.java | 19 +- .../server/issue/ws/AssignActionTest.java | 29 +- .../server/issue/ws/BulkChangeActionTest.java | 145 +-- .../issue/ws/DoTransitionActionTest.java | 35 +- .../server/issue/ws/IssueWsModuleTest.java | 2 +- .../issue/ws/SetSeverityActionTest.java | 17 +- .../server/issue/ws/SetTagsActionTest.java | 6 +- .../server/issue/ws/SetTypeActionTest.java | 32 +- .../IssueMetricFormulaFactoryImplTest.java | 832 ++++++++++++++++++ .../live/LiveMeasureComputerImplTest.java | 377 ++++++++ .../measure/live/LiveMeasureModuleTest.java | 37 + .../live/LiveQualityGateComputerImplTest.java | 198 +++++ .../measure/live/MeasureMatrixTest.java | 209 +++++ .../live/TestIssueMetricFormulaFactory.java | 43 + .../qualitygate/ConditionEvaluatorTest.java | 436 +++++++++ .../qualitygate/EvaluatedQualityGateTest.java | 41 +- .../LiveQualityGateFactoryImplTest.java | 186 ---- .../QualityGateConverterTest.java} | 11 +- .../qualitygate/QualityGateFinderTest.java | 8 +- .../qualitygate/QualityGateModuleTest.java | 2 +- .../QGChangeEventFactoryImplTest.java | 594 ------------- .../changeevent/QGChangeEventFactoryTest.java | 88 -- .../QGChangeEventListenersImplTest.java | 39 +- .../TestProjectConfigurationLoader.java | 45 + .../server/webhook/ProjectAnalysisTest.java | 7 +- .../WebhookPayloadFactoryImplTest.java | 7 +- .../WebhookQGChangeEventListenerTest.java | 3 +- .../ws/WebhookDeliveriesActionTest.java | 14 +- .../webhook/ws/WebhookDeliveryActionTest.java | 32 +- .../sonar/core/issue/IssueChangeContext.java | 5 - .../core/util/stream/MoreCollectors.java | 9 + .../resources/org/sonar/l10n/core.properties | 5 +- .../core/util/stream/MoreCollectorsTest.java | 21 +- .../org/sonar/api/measures/CoreMetrics.java | 22 +- .../org/sonar/api/resources/Qualifiers.java | 13 + .../LiveMeasuresTest/sonar-project.properties | 5 + .../src/file_with_one_line.xoo | 1 + .../src/file_with_three_lines.xoo | 3 + .../tests/issue/IssueMeasureTest.java | 6 +- .../tests/issue/IssueSearchTest.java | 10 +- .../tests/measure/LiveMeasuresTest.java | 198 +++++ .../qualityModel/ReliabilityMeasureTest.java | 10 +- .../sonarqube/tests/webhook/WebhooksTest.java | 61 +- .../one-bug-per-line-profile.xml | 12 + 153 files changed, 5497 insertions(+), 2144 deletions(-) create mode 100644 server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueGroupDto.java rename server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/{RatingGrid.java => DebtRatingGrid.java} (66%) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/Rating.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/issue/IssueChangePostProcessor.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/issue/IssueChangePostProcessorImpl.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueCounter.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueMetricFormula.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactory.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImpl.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveMeasureComputer.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveMeasureModule.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveQualityGateComputer.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveQualityGateComputerImpl.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/measure/live/package-info.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/qualitygate/ConditionEvaluator.java delete mode 100644 server/sonar-server/src/main/java/org/sonar/server/qualitygate/LiveQualityGateFactoryImpl.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateConverter.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateEvaluator.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateEvaluatorImpl.java delete mode 100644 server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventFactory.java delete mode 100644 server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventFactoryImpl.java rename server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/{RatingGridTest.java => DebtRatingGridTest.java} (88%) create mode 100644 server/sonar-server/src/test/java/org/sonar/server/issue/TestIssueChangePostProcessor.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImplTest.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/measure/live/LiveMeasureComputerImplTest.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/measure/live/LiveMeasureModuleTest.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/measure/live/LiveQualityGateComputerImplTest.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/measure/live/MeasureMatrixTest.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/measure/live/TestIssueMetricFormulaFactory.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/qualitygate/ConditionEvaluatorTest.java delete mode 100644 server/sonar-server/src/test/java/org/sonar/server/qualitygate/LiveQualityGateFactoryImplTest.java rename server/sonar-server/src/{main/java/org/sonar/server/qualitygate/LiveQualityGateFactory.java => test/java/org/sonar/server/qualitygate/QualityGateConverterTest.java} (84%) delete mode 100644 server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventFactoryImplTest.java delete mode 100644 server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventFactoryTest.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/settings/TestProjectConfigurationLoader.java create mode 100644 tests/projects/measure/LiveMeasuresTest/sonar-project.properties create mode 100644 tests/projects/measure/LiveMeasuresTest/src/file_with_one_line.xoo create mode 100644 tests/projects/measure/LiveMeasuresTest/src/file_with_three_lines.xoo create mode 100644 tests/src/test/java/org/sonarqube/tests/measure/LiveMeasuresTest.java create mode 100644 tests/src/test/resources/measure/LiveMeasuresTest/one-bug-per-line-profile.xml diff --git a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneBugIssuePerLineSensor.java b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneBugIssuePerLineSensor.java index fffd48a4de3..59574256742 100644 --- a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneBugIssuePerLineSensor.java +++ b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneBugIssuePerLineSensor.java @@ -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(); } diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java index 7f4cd20fcb8..8c0b8822e7f 100644 --- a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java +++ b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java @@ -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); diff --git a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java index 21c15533399..9c12b9f6101 100644 --- a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java +++ b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java @@ -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 diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDto.java index 25540e22faf..75d23fd49e5 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDto.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDto.java @@ -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 getUuidPathAsList() { + public List getUuidPathAsList() { return UUID_PATH_SPLITTER.splitToList(uuidPath); } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java index 2a20cc79dc8..5570e5c8494 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java @@ -107,6 +107,10 @@ public class IssueDao implements Dao { return executeLargeInputs(componentUuids, mapper(dbSession)::selectOpenByComponentUuids); } + public Collection selectIssueGroupsByBaseComponent(DbSession dbSession, ComponentDto baseComponent, long leakPeriodBeginningDate) { + return mapper(dbSession).selectIssueGroupsByBaseComponent(baseComponent, leakPeriodBeginningDate); + } + public void insert(DbSession session, IssueDto dto) { mapper(session).insert(dto); } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueGroupDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueGroupDto.java new file mode 100644 index 00000000000..0b8e259c271 --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueGroupDto.java @@ -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; + } +} diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java index ed0de50d912..9e6956976dd 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java @@ -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 handler); + + Collection selectIssueGroupsByBaseComponent( + @Param("baseComponent") ComponentDto baseComponent, + @Param("leakPeriodBeginningDate") long leakPeriodBeginningDate); } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/measure/LiveMeasureDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/measure/LiveMeasureDao.java index 17f3878cca9..14e8636561c 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/measure/LiveMeasureDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/measure/LiveMeasureDao.java @@ -42,7 +42,7 @@ public class LiveMeasureDao implements Dao { this.system2 = system2; } - public List selectByComponentUuids(DbSession dbSession, Collection largeComponentUuids, Collection metricIds) { + public List selectByComponentUuidsAndMetricIds(DbSession dbSession, Collection largeComponentUuids, Collection 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); } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/metric/MetricDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/metric/MetricDao.java index 160f43505a0..0f27ac2bae9 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/metric/MetricDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/metric/MetricDao.java @@ -47,7 +47,7 @@ public class MetricDao implements Dao { return mapper(session).selectByKey(key); } - public List selectByKeys(final DbSession session, List keys) { + public List selectByKeys(final DbSession session, Collection keys) { return executeLargeInputs(keys, mapper(session)::selectByKeys); } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/webhook/WebhookDeliveryDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/webhook/WebhookDeliveryDto.java index b414ecc7e18..91d076cb5dc 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/webhook/WebhookDeliveryDto.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/webhook/WebhookDeliveryDto.java @@ -39,12 +39,11 @@ public class WebhookDeliveryDto extends WebhookDeliveryLiteDto + + + + + + diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDtoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDtoTest.java index 92a9ef6a664..709308d9ce6 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDtoTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDtoTest.java @@ -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"); diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java index 1b948c72a65..c32c54e366a 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java @@ -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 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 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")); diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/measure/LiveMeasureDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/measure/LiveMeasureDaoTest.java index de232ef5a43..a37fc2ce3b3 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/measure/LiveMeasureDaoTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/measure/LiveMeasureDaoTest.java @@ -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 selected = underTest.selectByComponentUuids(db.getSession(), asList(measure1.getComponentUuid(), measure2.getComponentUuid()), singletonList(A_METRIC_ID)); + List 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 selected = underTest.selectByComponentUuids(db.getSession(), singletonList(measure.getComponentUuid()), singletonList(222)); + int otherMetricId = metric.getId() + 100; + List 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 selected = underTest.selectByComponentUuids(db.getSession(), singletonList("_missing_"), singletonList(measure.getMetricId())); + List 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 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 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 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 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 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 selected = underTest.selectByComponentUuids(db.getSession(), singletonList(dto.getComponentUuid()), singletonList(dto.getMetricId())); + List selected = underTest.selectByComponentUuidsAndMetricIds(db.getSession(), singletonList(dto.getComponentUuid()), singletonList(dto.getMetricId())); assertThat(selected).hasSize(1); assertThat(selected.get(0)).isEqualToComparingFieldByField(dto); } diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/purge/PurgeDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/purge/PurgeDaoTest.java index 49012759ba7..7a21a6b0850 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/purge/PurgeDaoTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/purge/PurgeDaoTest.java @@ -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) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexer.java index 71f40c3618c..86d055ccbba 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexer.java @@ -84,9 +84,10 @@ public class ComponentIndexer implements ProjectIndexer, NeedAuthorizationIndexe @Override public Collection prepareForRecovery(DbSession dbSession, Collection 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: diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ws/AppAction.java b/server/sonar-server/src/main/java/org/sonar/server/component/ws/AppAction.java index f4fa6bc5ab2..562ffd31f7a 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/ws/AppAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/ws/AppAction.java @@ -190,7 +190,7 @@ public class AppAction implements ComponentsWsAction { List metrics = dbClient.metricDao().selectByKeys(dbSession, METRIC_KEYS); Map metricsById = Maps.uniqueIndex(metrics, MetricDto::getId); List 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()); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/api/posttask/PostProjectAnalysisTasksExecutor.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/api/posttask/PostProjectAnalysisTasksExecutor.java index 7e2c720470d..8af751c52bc 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/api/posttask/PostProjectAnalysisTasksExecutor.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/api/posttask/PostProjectAnalysisTasksExecutor.java @@ -163,7 +163,7 @@ public class PostProjectAnalysisTasksExecutor implements ComputationStepExecutor @CheckForNull private QualityGateImpl createQualityGate() { - com.google.common.base.Optional qualityGateOptional = this.qualityGateHolder.getQualityGate(); + Optional qualityGateOptional = this.qualityGateHolder.getQualityGate(); if (qualityGateOptional.isPresent()) { org.sonar.server.computation.task.projectanalysis.qualitygate.QualityGate qualityGate = qualityGateOptional.get(); diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/formula/counter/RatingValue.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/formula/counter/RatingValue.java index d8e175cea19..03077e12a64 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/formula/counter/RatingValue.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/formula/counter/RatingValue.java @@ -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. diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/MutableQualityGateHolder.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/MutableQualityGateHolder.java index 722fd38894c..06f366cbb04 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/MutableQualityGateHolder.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/MutableQualityGateHolder.java @@ -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); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolder.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolder.java index 86f695c865f..ab4087034d4 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolder.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolder.java @@ -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 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 getEvaluation(); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolderImpl.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolderImpl.java index cbba9334d4b..ca83ac5eff5 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolderImpl.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolderImpl.java @@ -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; + 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 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 getQualityGate() { - checkState(initialized, "QualityGate has not been set yet"); - return qualityGate; + public Optional getEvaluation() { + checkState(evaluation != null, "Evaluation of QualityGate has not been set yet"); + return Optional.of(evaluation); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateService.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateService.java index c8fb00d37b0..580c4d58c27 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateService.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateService.java @@ -30,8 +30,9 @@ public interface QualityGateService { Optional 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 findDefaultQualityGate(Organization organizationDto); + QualityGate findDefaultQualityGate(Organization organizationDto); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateServiceImpl.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateServiceImpl.java index bb34218455e..bb4a3e0a36a 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateServiceImpl.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateServiceImpl.java @@ -57,13 +57,13 @@ public class QualityGateServiceImpl implements QualityGateService { } @Override - public Optional 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); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingGrid.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/DebtRatingGrid.java similarity index 66% rename from server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingGrid.java rename to server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/DebtRatingGrid.java index 0b33207cd0e..9114db76556 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingGrid.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/DebtRatingGrid.java @@ -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)); - } - } - } diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/MaintainabilityMeasuresVisitor.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/MaintainabilityMeasuresVisitor.java index 8670962407c..e707df6d9fc 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/MaintainabilityMeasuresVisitor.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/MaintainabilityMeasuresVisitor.java @@ -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 { 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
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)); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/NewMaintainabilityMeasuresVisitor.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/NewMaintainabilityMeasuresVisitor.java index 69aa35bf076..ff993ee8932 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/NewMaintainabilityMeasuresVisitor.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/NewMaintainabilityMeasuresVisitor.java @@ -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 r.getIndex() == index) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(format("Unknown value '%s'", index))); + } + + public static final Map RATING_BY_SEVERITY = ImmutableMap.of( + BLOCKER, E, + CRITICAL, D, + MAJOR, C, + MINOR, B, + INFO, A); +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingSettings.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingSettings.java index fa8669eddc4..023875ff2b8 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingSettings.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingSettings.java @@ -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 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 buildLanguageSpecificConfigurationByLanguageKey(Configuration config) { - ImmutableMap.Builder 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 initLanguageSpecificConfigurationByLanguageKey(Configuration config) { + ImmutableMap.Builder 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; diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/ReliabilityAndSecurityRatingMeasuresVisitor.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/ReliabilityAndSecurityRatingMeasuresVisitor.java index 64c5ce3d279..b9d55d835ff 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/ReliabilityAndSecurityRatingMeasuresVisitor.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/ReliabilityAndSecurityRatingMeasuresVisitor.java @@ -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 { - private static final Map 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 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 path) { - computeAndSaveMeasures(directory, path); + public void visitModule(Component module, Path path) { + computeAndSaveMeasures(module, path); } @Override - public void visitModule(Component module, Path path) { - computeAndSaveMeasures(module, path); + public void visitDirectory(Component directory, Path path) { + computeAndSaveMeasures(directory, path); } @Override @@ -110,27 +86,27 @@ public class ReliabilityAndSecurityRatingMeasuresVisitor extends PathAwareVisito private void computeAndSaveMeasures(Component component, Path 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 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 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 { diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/LoadQualityGateStep.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/LoadQualityGateStep.java index 6696baad527..7838e9091c2 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/LoadQualityGateStep.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/LoadQualityGateStep.java @@ -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 getShortLivingBranchQualityGate() { @@ -100,7 +96,7 @@ public class LoadQualityGateStep implements ComputationStep { } } - private Optional getOrganizationDefaultQualityGate() { + private QualityGate getOrganizationDefaultQualityGate() { return qualityGateService.findDefaultQualityGate(analysisMetadataHolder.getOrganization()); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/QualityGateMeasuresStep.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/QualityGateMeasuresStep.java index cd78e6a0e21..8fce93dbe39 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/QualityGateMeasuresStep.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/QualityGateMeasuresStep.java @@ -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> entry : conditionsPerMetric.asMap().entrySet()) { Metric metric = entry.getKey(); - Optional measure = measureRepository.getRawMeasure(project, metric); + com.google.common.base.Optional measure = measureRepository.getRawMeasure(project, metric); if (!measure.isPresent()) { continue; } diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTask.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTask.java index 16eea512659..576df71153e 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTask.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTask.java @@ -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); diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/ProjectIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/es/ProjectIndexer.java index 40e454e6de3..e236161d1da 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/es/ProjectIndexer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/es/ProjectIndexer.java @@ -40,7 +40,8 @@ public interface ProjectIndexer extends ResilientIndexer { PROJECT_DELETION, PROJECT_KEY_UPDATE, PROJECT_TAGS_UPDATE, - PERMISSION_CHANGE + PERMISSION_CHANGE, + MEASURE_CHANGE } /** diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/AbstractChangeTagsAction.java b/server/sonar-server/src/main/java/org/sonar/server/issue/AbstractChangeTagsAction.java index 4df063c29d9..ac81874ef50 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/AbstractChangeTagsAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/AbstractChangeTagsAction.java @@ -60,6 +60,11 @@ public abstract class AbstractChangeTagsAction extends Action { protected abstract Collection getTagsToSet(Context context, Collection tagsFromParams); + @Override + public boolean shouldRefreshMeasures() { + return false; + } + private Set parseTags(Map properties) { Set result = new HashSet<>(); String tagsString = (String) properties.get(TAGS_PARAMETER); diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/Action.java b/server/sonar-server/src/main/java/org/sonar/server/issue/Action.java index 5177042d609..24f00e1895e 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/Action.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/Action.java @@ -75,6 +75,8 @@ public abstract class Action { public abstract boolean execute(Map properties, Context context); + public abstract boolean shouldRefreshMeasures(); + public interface Context { DefaultIssue issue(); diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/AssignAction.java b/server/sonar-server/src/main/java/org/sonar/server/issue/AssignAction.java index 7c522fc3286..90aa92e568b 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/AssignAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/AssignAction.java @@ -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 properties, Context context) { return assignee == null || ((Set) properties.get(ASSIGNEE_ORGANIZATIONS)).contains(context.project().getOrganizationUuid()); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/CommentAction.java b/server/sonar-server/src/main/java/org/sonar/server/issue/CommentAction.java index 45ceb028701..bab386354c5 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/CommentAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/CommentAction.java @@ -51,6 +51,11 @@ public class CommentAction extends Action { return true; } + @Override + public boolean shouldRefreshMeasures() { + return false; + } + private static String comment(Map properties) { String param = (String) properties.get(COMMENT_PROPERTY); if (Strings.isNullOrEmpty(param)) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/IssueChangePostProcessor.java b/server/sonar-server/src/main/java/org/sonar/server/issue/IssueChangePostProcessor.java new file mode 100644 index 00000000000..b0d28c613f2 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/IssueChangePostProcessor.java @@ -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 changedIssues, Collection components); + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/IssueChangePostProcessorImpl.java b/server/sonar-server/src/main/java/org/sonar/server/issue/IssueChangePostProcessorImpl.java new file mode 100644 index 00000000000..c19e9ba3b93 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/IssueChangePostProcessorImpl.java @@ -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 changedIssues, Collection components) { + List gateChangeEvents = liveMeasureComputer.refresh(dbSession, components); + qualityGateListeners.broadcastOnIssueChange(changedIssues, gateChangeEvents); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/IssueUpdater.java b/server/sonar-server/src/main/java/org/sonar/server/issue/IssueUpdater.java index 8f3aa25c0b7..8dac9d11f00 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/IssueUpdater.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/IssueUpdater.java @@ -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 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 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 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 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 rule, ComponentDto project, ComponentDto component) { IssueDto issueDto = issueStorage.save(session, issue); notificationService.scheduleForSending(new IssueChangeNotification() @@ -87,7 +99,7 @@ public class IssueUpdater { } private Optional getRuleByKey(DbSession session, RuleKey ruleKey) { - Optional rule = Optional.ofNullable(dbClient.ruleDao().selectDefinitionByKey(session, ruleKey).orElse(null)); + Optional rule = dbClient.ruleDao().selectDefinitionByKey(session, ruleKey); return (rule.isPresent() && rule.get().getStatus() != RuleStatus.REMOVED) ? rule : Optional.empty(); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/SetSeverityAction.java b/server/sonar-server/src/main/java/org/sonar/server/issue/SetSeverityAction.java index d1909e9c1ec..3b905d1011f 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/SetSeverityAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/SetSeverityAction.java @@ -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 properties) { String param = (String) properties.get(SEVERITY_PARAMETER); checkArgument(!isNullOrEmpty(param), "Missing parameter : '%s'", SEVERITY_PARAMETER); diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/SetTypeAction.java b/server/sonar-server/src/main/java/org/sonar/server/issue/SetTypeAction.java index 26909a4f867..193fe499247 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/SetTypeAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/SetTypeAction.java @@ -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 properties) { String type = (String) properties.get(TYPE_PARAMETER); checkArgument(!isNullOrEmpty(type), "Missing parameter : '%s'", TYPE_PARAMETER); diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/TransitionAction.java b/server/sonar-server/src/main/java/org/sonar/server/issue/TransitionAction.java index 23c44754b45..444b105017b 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/TransitionAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/TransitionAction.java @@ -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() diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndexer.java index 9b430a5f25e..308afd0df57 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndexer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndexer.java @@ -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: diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/AddCommentAction.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/AddCommentAction.java index 4993dca94e1..d501ed05426 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/AddCommentAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/AddCommentAction.java @@ -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); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/AssignAction.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/AssignAction.java index a65810ac2a8..8cd574cca63 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/AssignAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/AssignAction.java @@ -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); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java index 1edfaf4ab3e..11ed5626561 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java @@ -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 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 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 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 executeBulkChange() { - return bulkChangeData -> { - BulkChangeResult result = new BulkChangeResult(bulkChangeData.issues.size()); - IssueChangeContext issueChangeContext = IssueChangeContext.createUser(new Date(system2.now()), userSession.getLogin()); + List items = bulkChangeData.issues.stream() + .filter(bulkChange(issueChangeContext, bulkChangeData, result)) + .collect(MoreCollectors.toList()); + issueStorage.save(items); - List 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 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 buildWebhookIssueChange(Map> 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 touchedComponentUuids = result.success.stream() + .map(DefaultIssue::componentUuid) + .collect(Collectors.toSet()); + List touchedComponents = touchedComponentUuids.stream() + .map(data.componentsByUuid::get) + .collect(MoreCollectors.toList(touchedComponentUuids.size())); + + List changedIssues = data.issues.stream().filter(result.success::contains).collect(MoreCollectors.toList()); + issueChangePostProcessor.process(dbSession, changedIssues, touchedComponents); } private static Predicate 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 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 success = new HashSet<>(); + private final Set 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; } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java index ba9b4a0c541..501d7f03fa7 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java @@ -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 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); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java index 7d4e1985a96..a434b524eed 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java @@ -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); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetSeverityAction.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetSeverityAction.java index d7407b48d44..c1cd538704b 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetSeverityAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetSeverityAction.java @@ -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); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTagsAction.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTagsAction.java index 778c03b681b..004e916a012 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTagsAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTagsAction.java @@ -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); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java index 4239996386e..d22d5fa26e0 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java @@ -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 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); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexer.java index 7c4651cc453..bac783b89e3 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexer.java @@ -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 projectUuids) { + List 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 items) { if (items.isEmpty()) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueCounter.java b/server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueCounter.java new file mode 100644 index 00000000000..09e1e57a768 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueCounter.java @@ -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 highestSeverityOfUnresolved = new EnumMap<>(RuleType.class); + private final Map effortOfUnresolved = new EnumMap<>(RuleType.class); + private final Map unresolvedBySeverity = new HashMap<>(); + private final Map unresolvedByType = new EnumMap<>(RuleType.class); + private final Map byResolution = new HashMap<>(); + private final Map byStatus = new HashMap<>(); + private final Count unresolved = new Count(); + + IssueCounter(Collection 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 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); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueMetricFormula.java b/server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueMetricFormula.java new file mode 100644 index 00000000000..521e65be629 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueMetricFormula.java @@ -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 formula; + private final Collection dependentMetrics; + + IssueMetricFormula(Metric metric, boolean onLeak, BiConsumer formula) { + this(metric, onLeak, formula, emptyList()); + } + + IssueMetricFormula(Metric metric, boolean onLeak, BiConsumer formula, Collection dependentMetrics) { + this.metric = metric; + this.onLeak = onLeak; + this.formula = formula; + this.dependentMetrics = dependentMetrics; + } + + Metric getMetric() { + return metric; + } + + boolean isOnLeak() { + return onLeak; + } + + Collection 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 getValue(Metric metric); + + Optional getLeakValue(Metric metric); + + void setValue(double value); + + void setValue(Rating value); + + void setLeakValue(double value); + + void setLeakValue(Rating value); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactory.java b/server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactory.java new file mode 100644 index 00000000000..9fc2578e9fe --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactory.java @@ -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 getFormulas(); + + Set getFormulaMetrics(); + + static Set extractMetrics(List formulas) { + return formulas.stream() + .flatMap(f -> Stream.concat(Stream.of(f.getMetric()), f.getDependentMetrics().stream())) + .collect(Collectors.toSet()); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImpl.java b/server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImpl.java new file mode 100644 index 00000000000..4044e5d7ea4 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImpl.java @@ -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 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 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 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 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 getFormulas() { + return FORMULAS; + } + + @Override + public Set getFormulaMetrics() { + return FORMULA_METRICS; + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveMeasureComputer.java b/server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveMeasureComputer.java new file mode 100644 index 00000000000..74b51a0e0be --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveMeasureComputer.java @@ -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 refresh(DbSession dbSession, Collection components); + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java b/server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java new file mode 100644 index 00000000000..3f6f986c236 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java @@ -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 refresh(DbSession dbSession, Collection components) { + if (components.isEmpty()) { + return emptyList(); + } + + List result = new ArrayList<>(); + Map> componentsByProjectUuid = components.stream().collect(groupingBy(ComponentDto::projectUuid)); + for (List groupedComponents : componentsByProjectUuid.values()) { + Optional qgChangeEvent = refreshComponentsOnSameProject(dbSession, groupedComponents); + qgChangeEvent.ifPresent(result::add); + } + return result; + } + + private Optional refreshComponentsOnSameProject(DbSession dbSession, List touchedComponents) { + // load all the components to be refreshed, including their ancestors + List components = loadTreeOfComponents(dbSession, touchedComponents); + ComponentDto project = findProject(components); + OrganizationDto organization = loadOrganization(dbSession, project); + BranchDto branch = loadBranch(dbSession, project); + + Optional lastAnalysis = dbClient.snapshotDao().selectLastAnalysisByRootComponentUuid(dbSession, project.uuid()); + if (!lastAnalysis.isPresent()) { + return Optional.empty(); + } + Optional beginningOfLeakPeriod = lastAnalysis.map(SnapshotDto::getPeriodDate); + + QualityGate qualityGate = qGateComputer.loadQualityGate(dbSession, organization, project, branch); + Collection metricKeys = getKeysOfAllInvolvedMetrics(qualityGate); + + Map metricsPerId = dbClient.metricDao().selectByKeys(dbSession, metricKeys).stream() + .collect(uniqueIndex(MetricDto::getId)); + List componentUuids = components.stream().map(ComponentDto::uuid).collect(toArrayList(components.size())); + List 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 loadTreeOfComponents(DbSession dbSession, List touchedComponents) { + Set 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 getKeysOfAllInvolvedMetrics(QualityGate gate) { + Set metricKeys = new HashSet<>(); + for (Metric metric : formulaFactory.getFormulaMetrics()) { + metricKeys.add(metric.getKey()); + } + metricKeys.addAll(qGateComputer.getMetricsRelatedTo(gate)); + return metricKeys; + } + + private static ComponentDto findProject(Collection 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 getValue(Metric metric) { + Optional measure = matrix.getMeasure(currentComponent, metric.getKey()); + return measure.map(LiveMeasureDto::getValue); + } + + @Override + public Optional getLeakValue(Metric metric) { + Optional measure = matrix.getMeasure(currentComponent, metric.getKey()); + return measure.map(LiveMeasureDto::getVariation); + } + + @Override + public void setValue(double value) { + String metricKey = currentFormula.getMetric().getKey(); + checkState(!currentFormula.isOnLeak(), "Formula of metric %s accepts only leak values", metricKey); + matrix.setValue(currentComponent, metricKey, value); + } + + @Override + public void setLeakValue(double value) { + String metricKey = currentFormula.getMetric().getKey(); + checkState(currentFormula.isOnLeak(), "Formula of metric %s does not accept leak values", metricKey); + matrix.setLeakValue(currentComponent, metricKey, value); + } + + @Override + public void setValue(Rating value) { + String metricKey = currentFormula.getMetric().getKey(); + checkState(!currentFormula.isOnLeak(), "Formula of metric %s accepts only leak values", metricKey); + matrix.setValue(currentComponent, metricKey, value); + } + + @Override + public void setLeakValue(Rating value) { + String metricKey = currentFormula.getMetric().getKey(); + checkState(currentFormula.isOnLeak(), "Formula of metric %s does not accept leak values", metricKey); + matrix.setLeakValue(currentComponent, metricKey, value); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveMeasureModule.java b/server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveMeasureModule.java new file mode 100644 index 00000000000..116bd907c09 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveMeasureModule.java @@ -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); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveQualityGateComputer.java b/server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveQualityGateComputer.java new file mode 100644 index 00000000000..e59d5d359df --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveQualityGateComputer.java @@ -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 getMetricsRelatedTo(QualityGate gate); + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveQualityGateComputerImpl.java b/server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveQualityGateComputerImpl.java new file mode 100644 index 00000000000..b7cf692c7e8 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveQualityGateComputerImpl.java @@ -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 conditionDtos = dbClient.gateConditionDao().selectForQualityGate(dbSession, gateDto.getId()); + Set metricIds = conditionDtos.stream().map(c -> (int) c.getMetricId()) + .collect(toHashSet(conditionDtos.size())); + Map metricsById = dbClient.metricDao().selectByIds(dbSession, metricIds).stream() + .collect(uniqueIndex(MetricDto::getId)); + + Set 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 = 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 getMetricsRelatedTo(QualityGate gate) { + Set 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 getStringValue() { + return Optional.ofNullable(dto.getTextValue()); + } + + @Override + public OptionalDouble getLeakValue() { + if (dto.getVariation() == null) { + return OptionalDouble.empty(); + } + return OptionalDouble.of(dto.getVariation()); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java b/server/sonar-server/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java new file mode 100644 index 00000000000..0dad8f5b745 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java @@ -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: + *
    + *
  • the values of last analysis, restricted to the needed metrics
  • + *
  • the refreshed values
  • + *
+ */ +class MeasureMatrix { + + // component uuid -> metric key -> measure + private final Table table; + + private final Map metricsByKeys = new HashMap<>(); + private final Map metricsByIds = new HashMap<>(); + + MeasureMatrix(Collection components, Collection metrics, List 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 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 getChanged() { + return table.values() + .stream() + .filter(Objects::nonNull) + .filter(MeasureCell::isChanged) + .map(MeasureCell::getMeasure); + } + + private void changeCell(ComponentDto component, String metricKey, Function 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; + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/live/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/measure/live/package-info.java new file mode 100644 index 00000000000..4e4f17543be --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/live/package-info.java @@ -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; diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentAction.java b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentAction.java index a4a59852c7f..8ede31ea203 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentAction.java @@ -216,7 +216,7 @@ public class ComponentAction implements MeasuresWsAction { private List searchMeasures(DbSession dbSession, ComponentDto component, List metrics) { List metricIds = Lists.transform(metrics, MetricDto::getId); - List measures = dbClient.liveMeasureDao().selectByComponentUuids(dbSession, singletonList(component.uuid()), metricIds); + List measures = dbClient.liveMeasureDao().selectByComponentUuidsAndMetricIds(dbSession, singletonList(component.uuid()), metricIds); addBestValuesToMeasures(measures, component, metrics); return measures; } diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/SearchAction.java b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/SearchAction.java index 30424bc006b..b58abdfb97f 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/measure/ws/SearchAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/ws/SearchAction.java @@ -162,7 +162,7 @@ public class SearchAction implements MeasuresWsAction { } private List 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()))); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java index c3b7f556000..1a9ab35efec 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java @@ -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 prepareForRecovery(DbSession dbSession, Collection 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: diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java index dfbee585adf..db08d7499b5 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java @@ -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, diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/Condition.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/Condition.java index 26a2691d5ef..3319b92dd4a 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/Condition.java +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/Condition.java @@ -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)); + } } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/ConditionEvaluator.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/ConditionEvaluator.java new file mode 100644 index 00000000000..f154d3414fd --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/ConditionEvaluator.java @@ -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 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 measure = measures.get(condition.getMetricKey()); + if (!measure.isPresent()) { + return new EvaluatedCondition(condition, EvaluationStatus.OK, null); + } + + Optional 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 evaluateCondition(Condition condition, ValueType type, Comparable value, boolean error) { + Optional 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 getThreshold(Condition condition, ValueType valueType, boolean error) { + Optional 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 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())); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/EvaluatedCondition.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/EvaluatedCondition.java index 97aa900139e..b47c176ac92 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/EvaluatedCondition.java +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/EvaluatedCondition.java @@ -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 + '\'')) + '}'; } diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/EvaluatedQualityGate.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/EvaluatedQualityGate.java index e262edcfb3b..f568d242c76 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/EvaluatedQualityGate.java +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/EvaluatedQualityGate.java @@ -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 evaluatedConditions; + private final boolean ignoredConditionsOnSmallChangeset; - private EvaluatedQualityGate(QualityGate qualityGate, Status status, Set evaluatedConditions) { - this.qualityGate = qualityGate; - this.status = status; + private EvaluatedQualityGate(QualityGate qualityGate, Metric.Level status, Set 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 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 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 checkEvaluatedConditions(QualityGate qualityGate, Map 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 } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/LiveQualityGateFactoryImpl.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/LiveQualityGateFactoryImpl.java deleted file mode 100644 index 559827250bf..00000000000 --- a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/LiveQualityGateFactoryImpl.java +++ /dev/null @@ -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 typeFacet = new Facets(searchResponse, system2.getDefaultTimeZone()) - .get(RuleIndex.FACET_TYPES); - - EvaluatedQualityGate.Builder builder = EvaluatedQualityGate.newBuilder(); - Set 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 conditions) { - if (conditions.stream().anyMatch(c -> c.getStatus() == EvaluatedCondition.EvaluationStatus.ERROR)) { - return EvaluatedQualityGate.Status.ERROR; - } - return EvaluatedQualityGate.Status.OK; - } - - private static long getMeasure(LinkedHashMap 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 facet, RuleType ruleType) { - Long res = facet.get(ruleType.name()); - if (res == null) { - return 0L; - } - return res; - } -} diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateConditionsUpdater.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateConditionsUpdater.java index a269d519ad2..9d56caabfb7 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateConditionsUpdater.java +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateConditionsUpdater.java @@ -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; diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateConverter.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateConverter.java new file mode 100644 index 00000000000..785ac31bb40 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateConverter.java @@ -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; + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateEvaluator.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateEvaluator.java new file mode 100644 index 00000000000..3ecf03d08dd --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateEvaluator.java @@ -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 getMetricKeys(QualityGate gate); + + interface Measures { + Optional get(String metricKey); + } + + interface Measure { + Metric.ValueType getType(); + + OptionalDouble getValue(); + + Optional getStringValue(); + + OptionalDouble getLeakValue(); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateEvaluatorImpl.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateEvaluatorImpl.java new file mode 100644 index 00000000000..f0ce9449320 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateEvaluatorImpl.java @@ -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 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 conditionsPerMetric = gate.getConditions().stream() + .collect(MoreCollectors.index(Condition::getMetricKey, Function.identity())); + + for (Map.Entry> entry : conditionsPerMetric.asMap().entrySet()) { + String metricKey = entry.getKey(); + Collection 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 getMetricKeys(QualityGate gate) { + Set 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 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 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 conditions) { + Set 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; + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateFinder.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateFinder.java index 68dc868d3ef..d1abced201e 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateFinder.java +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateFinder.java @@ -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 qualityGateId = dbClient.projectQgateAssociationDao().selectQGateIdByComponentId(dbSession, componentId); + public QualityGateData getQualityGate(DbSession dbSession, OrganizationDto organization, ComponentDto component) { + Optional 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) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateModule.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateModule.java index 6a7626518b0..46f48b32c98 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateModule.java @@ -45,6 +45,7 @@ public class QualityGateModule extends Module { QualityGateUpdater.class, QualityGateConditionsUpdater.class, QualityGateFinder.class, + QualityGateEvaluatorImpl.class, // WS QualityGatesWsSupport.class, QualityGatesWs.class, diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/RegisterQualityGates.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/RegisterQualityGates.java index 9cc85232103..3effb8aa841 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/RegisterQualityGates.java +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/RegisterQualityGates.java @@ -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 QUALITY_GATE_CONDITIONS = asList( new QualityGateCondition().setMetricKey(NEW_SECURITY_RATING_KEY).setOperator(OPERATOR_GREATER_THAN).setPeriod(LEAK_PERIOD).setErrorThreshold(A_RATING), diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/ShortLivingBranchQualityGate.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/ShortLivingBranchQualityGate.java index 336568df8eb..b086408aa92 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/ShortLivingBranchQualityGate.java +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/ShortLivingBranchQualityGate.java @@ -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 } diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventFactory.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventFactory.java deleted file mode 100644 index 007204554ac..00000000000 --- a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventFactory.java +++ /dev/null @@ -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 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 getRuleType() { - return Optional.ofNullable(ruleType); - } - - public Optional 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 issues; - private final List components; - - public IssueChangeData(List issues, List components) { - this.issues = ImmutableList.copyOf(issues); - this.components = ImmutableList.copyOf(components); - } - - public List getIssues() { - return issues; - } - - public List 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 + - '}'; - } - } -} diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventFactoryImpl.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventFactoryImpl.java deleted file mode 100644 index b1f32639bd8..00000000000 --- a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventFactoryImpl.java +++ /dev/null @@ -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 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 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 from(IssueChangeData issueChangeData) { - try (DbSession dbSession = dbClient.openSession(false)) { - Map branchesByUuid = getBranchComponents(dbSession, issueChangeData); - if (branchesByUuid.isEmpty()) { - return emptyList(); - } - - Set branchProjectUuids = branchesByUuid.values().stream() - .map(ComponentDto::uuid) - .collect(toSet(branchesByUuid.size())); - Set shortBranches = dbClient.branchDao().selectByUuids(dbSession, branchProjectUuids) - .stream() - .filter(branchDto -> branchDto.getBranchType() == BranchType.SHORT) - .collect(toSet(branchesByUuid.size())); - if (shortBranches.isEmpty()) { - return emptyList(); - } - - Map configurationByUuid = projectConfigurationLoader.loadProjectConfigurations(dbSession, - shortBranches.stream().map(shortBranch -> branchesByUuid.get(shortBranch.getUuid())).collect(Collectors.toSet())); - Set shortBranchesComponentUuids = shortBranches.stream().map(BranchDto::getUuid).collect(toSet(shortBranches.size())); - Map 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 getBranchComponents(DbSession dbSession, IssueChangeData issueChangeData) { - Set projectUuids = issueChangeData.getIssues().stream() - .map(DefaultIssue::projectUuid) - .collect(toSet()); - Set 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)); - } -} diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java index 791164c7cd9..46921a78348 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java @@ -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 qgChangeEvents); + void broadcastOnIssueChange(List changedIssues, Collection qgChangeEvents); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java index f471916f41c..b2fe05c836f 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java @@ -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 changeEvents) { - if (listeners.length == 0 || issueChangeData.getComponents().isEmpty() || issueChangeData.getIssues().isEmpty() || changeEvents.isEmpty()) { + public void broadcastOnIssueChange(List issues, Collection changeEvents) { + if (listeners.length == 0 || issues.isEmpty() || changeEvents.isEmpty()) { return; } try { Multimap eventsByComponentUuid = changeEvents.stream() .collect(MoreCollectors.index(t -> t.getProject().uuid())); - Multimap issueByComponentUuid = issueChangeData.getIssues().stream() + Multimap issueByComponentUuid = issues.stream() .collect(MoreCollectors.index(DefaultIssue::projectUuid)); issueByComponentUuid.asMap() diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/ws/GetByProjectAction.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/ws/GetByProjectAction.java index c5d07861053..5b78ed31c9f 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/ws/GetByProjectAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/ws/GetByProjectAction.java @@ -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); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/settings/ProjectConfigurationLoader.java b/server/sonar-server/src/main/java/org/sonar/server/settings/ProjectConfigurationLoader.java index 356eb87c43b..32123e9ac05 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/settings/ProjectConfigurationLoader.java +++ b/server/sonar-server/src/main/java/org/sonar/server/settings/ProjectConfigurationLoader.java @@ -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 loadProjectConfigurations(DbSession dbSession, Set projects); + + default Configuration loadProjectConfiguration(DbSession dbSession, ComponentDto project) { + Map configurations = loadProjectConfigurations(dbSession, Collections.singleton(project)); + return requireNonNull(configurations.get(project.uuid()), () -> format("Configuration for project '%s' is not found", project.getKey())); + } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/test/index/TestIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/test/index/TestIndexer.java index 5c96aa6479d..01e8bcbaa08 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/test/index/TestIndexer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/test/index/TestIndexer.java @@ -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: diff --git a/server/sonar-server/src/main/java/org/sonar/server/ui/ws/ComponentAction.java b/server/sonar-server/src/main/java/org/sonar/server/ui/ws/ComponentAction.java index 79ea6d75af9..580a62cd94a 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/ui/ws/ComponentAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/ui/ws/ComponentAction.java @@ -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()) diff --git a/server/sonar-server/src/main/java/org/sonar/server/webhook/WebhookQGChangeEventListener.java b/server/sonar-server/src/main/java/org/sonar/server/webhook/WebhookQGChangeEventListener.java index 5f101568583..85c1a460e95 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/webhook/WebhookQGChangeEventListener.java +++ b/server/sonar-server/src/main/java/org/sonar/server/webhook/WebhookQGChangeEventListener.java @@ -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 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); diff --git a/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookWsSupport.java b/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookWsSupport.java index 48b5b99bd70..8eab7f02c95 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookWsSupport.java +++ b/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookWsSupport.java @@ -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; } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/formula/counter/RatingValueTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/formula/counter/RatingValueTest.java index a456e2b84ed..742bb1d35e9 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/formula/counter/RatingValueTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/formula/counter/RatingValueTest.java @@ -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 { diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/measure/BestValueOptimizationTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/measure/BestValueOptimizationTest.java index ac7f9efdfdc..e023123bc95 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/measure/BestValueOptimizationTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/measure/BestValueOptimizationTest.java @@ -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 { diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitygate/MutableQualityGateHolderRule.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitygate/MutableQualityGateHolderRule.java index 5db2a72cc8c..cd1c045d9d3 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitygate/MutableQualityGateHolderRule.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitygate/MutableQualityGateHolderRule.java @@ -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 getQualityGate() { + return delegate.getQualityGate(); } @Override - public Optional getQualityGate() { - return delegate.getQualityGate(); + public void setEvaluation(EvaluatedQualityGate evaluation) { + delegate.setEvaluation(evaluation); + } + + @Override + public Optional getEvaluation() { + return delegate.getEvaluation(); } @Override diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolderImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolderImplTest.java index 24e6a1a7be3..a53912856e3 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolderImplTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolderImplTest.java @@ -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.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); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolderRule.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolderRule.java index 16485fd5d1a..10a0c320eb5 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolderRule.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolderRule.java @@ -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; + @Nullable + private Optional 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 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; } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingGridTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/DebtRatingGridTest.java similarity index 88% rename from server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingGridTest.java rename to server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/DebtRatingGridTest.java index 7149a2e9ca1..596697900d9 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingGridTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/DebtRatingGridTest.java @@ -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 diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/MaintainabilityMeasuresVisitorTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/MaintainabilityMeasuresVisitorTest.java index f3b31742c57..b77f3a0780b 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/MaintainabilityMeasuresVisitorTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/MaintainabilityMeasuresVisitorTest.java @@ -31,7 +31,6 @@ import org.sonar.server.computation.task.projectanalysis.issue.ComponentIssuesRe import org.sonar.server.computation.task.projectanalysis.measure.Measure; import org.sonar.server.computation.task.projectanalysis.measure.MeasureRepositoryRule; import org.sonar.server.computation.task.projectanalysis.metric.MetricRepositoryRule; -import org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; @@ -57,9 +56,9 @@ import static org.sonar.server.computation.task.projectanalysis.component.Report import static org.sonar.server.computation.task.projectanalysis.measure.Measure.newMeasureBuilder; import static org.sonar.server.computation.task.projectanalysis.measure.MeasureRepoEntry.entryOf; import static org.sonar.server.computation.task.projectanalysis.measure.MeasureRepoEntry.toEntries; -import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.A; -import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.C; -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.C; +import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.E; public class MaintainabilityMeasuresVisitorTest { @@ -114,7 +113,7 @@ public class MaintainabilityMeasuresVisitorTest { @Before public void setUp() { // assumes rating configuration is consistent - when(ratingSettings.getRatingGrid()).thenReturn(new RatingGrid(RATING_GRID)); + when(ratingSettings.getDebtRatingGrid()).thenReturn(new DebtRatingGrid(RATING_GRID)); when(ratingSettings.getDevCost(LANGUAGE_KEY_1)).thenReturn(DEV_COST_LANGUAGE_1); when(ratingSettings.getDevCost(LANGUAGE_KEY_2)).thenReturn(DEV_COST_LANGUAGE_2); diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/NewMaintainabilityMeasuresVisitorTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/NewMaintainabilityMeasuresVisitorTest.java index 8d0d84ee4a0..c39cd3146db 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/NewMaintainabilityMeasuresVisitorTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/NewMaintainabilityMeasuresVisitorTest.java @@ -39,7 +39,6 @@ import org.sonar.server.computation.task.projectanalysis.measure.MeasureReposito import org.sonar.server.computation.task.projectanalysis.metric.MetricRepositoryRule; import org.sonar.server.computation.task.projectanalysis.period.Period; import org.sonar.server.computation.task.projectanalysis.period.PeriodHolderRule; -import org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating; import org.sonar.server.computation.task.projectanalysis.scm.Changeset; import org.sonar.server.computation.task.projectanalysis.scm.ScmInfoRepositoryRule; @@ -48,6 +47,8 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.sonar.api.measures.CoreMetrics.NCLOC_DATA; import static org.sonar.api.measures.CoreMetrics.NCLOC_DATA_KEY; +import static org.sonar.api.measures.CoreMetrics.NEW_DEVELOPMENT_COST; +import static org.sonar.api.measures.CoreMetrics.NEW_DEVELOPMENT_COST_KEY; import static org.sonar.api.measures.CoreMetrics.NEW_MAINTAINABILITY_RATING; import static org.sonar.api.measures.CoreMetrics.NEW_MAINTAINABILITY_RATING_KEY; import static org.sonar.api.measures.CoreMetrics.NEW_SQALE_DEBT_RATIO; @@ -60,8 +61,8 @@ import static org.sonar.server.computation.task.projectanalysis.component.Compon import static org.sonar.server.computation.task.projectanalysis.component.Component.Type.PROJECT; import static org.sonar.server.computation.task.projectanalysis.measure.Measure.newMeasureBuilder; import static org.sonar.server.computation.task.projectanalysis.measure.MeasureAssert.assertThat; -import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.A; -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.D; public class NewMaintainabilityMeasuresVisitorTest { @@ -85,7 +86,8 @@ public class NewMaintainabilityMeasuresVisitorTest { .add(NEW_TECHNICAL_DEBT) .add(NCLOC_DATA) .add(NEW_SQALE_DEBT_RATIO) - .add(NEW_MAINTAINABILITY_RATING); + .add(NEW_MAINTAINABILITY_RATING) + .add(NEW_DEVELOPMENT_COST); @Rule public MeasureRepositoryRule measureRepository = MeasureRepositoryRule.create(treeRootHolder, metricRepository); @Rule @@ -97,7 +99,7 @@ public class NewMaintainabilityMeasuresVisitorTest { @Before public void setUp() throws Exception { - when(ratingSettings.getRatingGrid()).thenReturn(new RatingGrid(RATING_GRID)); + when(ratingSettings.getDebtRatingGrid()).thenReturn(new DebtRatingGrid(RATING_GRID)); underTest = new VisitorsCrawler(Arrays.asList(new NewMaintainabilityMeasuresVisitor(metricRepository, measureRepository, scmInfoRepository, periodsHolder, ratingSettings))); } @@ -320,7 +322,7 @@ public class NewMaintainabilityMeasuresVisitorTest { } @Test - public void compute_new_maintainability_rating() throws Exception { + public void compute_new_maintainability_rating() { setPeriod(); when(ratingSettings.getDevCost(LANGUAGE_1_KEY)).thenReturn(LANGUAGE_1_DEV_COST); treeRootHolder.setRoot( @@ -354,7 +356,41 @@ public class NewMaintainabilityMeasuresVisitorTest { } @Test - public void compute_new_maintainability_rating_to_A_when_no_debt() throws Exception { + public void compute_new_development_cost() { + setPeriod(); + when(ratingSettings.getDevCost(LANGUAGE_1_KEY)).thenReturn(LANGUAGE_1_DEV_COST); + treeRootHolder.setRoot( + builder(PROJECT, ROOT_REF) + .addChildren( + builder(MODULE, 11) + .addChildren( + builder(DIRECTORY, 111) + .addChildren( + builder(FILE, LANGUAGE_1_FILE_REF).setFileAttributes(new FileAttributes(false, LANGUAGE_1_KEY, 4)).build(), + builder(FILE, 22_222).setFileAttributes(new FileAttributes(false, LANGUAGE_1_KEY, 6)).build() + ) + .build()) + .build()) + .build()); + + // 4 lines file, only first one is not ncloc + measureRepository.addRawMeasure(LANGUAGE_1_FILE_REF, NCLOC_DATA_KEY, createNclocDataMeasure(2, 3, 4)); + // first 2 lines are before all snapshots, 2 last lines are after PERIOD 2's snapshot date + scmInfoRepository.setScmInfo(LANGUAGE_1_FILE_REF, createChangesets(PERIOD_SNAPSHOT_DATE - 100, 2, PERIOD_SNAPSHOT_DATE + 100, 2)); + // 6 lines file, only last one is not ncloc + measureRepository.addRawMeasure(22_222, NCLOC_DATA_KEY, createNclocDataMeasure(1, 2, 3, 4, 5)); + // first 2 lines are before all snapshots, 4 last lines are after PERIOD 2's snapshot date + scmInfoRepository.setScmInfo(22_222, createChangesets(PERIOD_SNAPSHOT_DATE - 100, 2, PERIOD_SNAPSHOT_DATE + 100, 4)); + + underTest.visit(treeRootHolder.getRoot()); + + assertNewDevelopmentCostValues(ROOT_REF, 5 * LANGUAGE_1_DEV_COST); + assertNewDevelopmentCostValues(LANGUAGE_1_FILE_REF, 2 * LANGUAGE_1_DEV_COST); + assertNewDevelopmentCostValues(22_222, 3 * LANGUAGE_1_DEV_COST); + } + + @Test + public void compute_new_maintainability_rating_to_A_when_no_debt() { setPeriod(); when(ratingSettings.getDevCost(LANGUAGE_1_KEY)).thenReturn(LANGUAGE_1_DEV_COST); treeRootHolder.setRoot( @@ -471,6 +507,10 @@ public class NewMaintainabilityMeasuresVisitorTest { assertThat(measureRepository.getAddedRawMeasure(componentRef, NEW_SQALE_DEBT_RATIO_KEY)).hasVariation(expectedVariation, VARIATION_COMPARISON_OFFSET); } + private void assertNewDevelopmentCostValues(int componentRef, long expectedVariation) { + assertThat(measureRepository.getAddedRawMeasure(componentRef, NEW_DEVELOPMENT_COST_KEY)).hasVariation(expectedVariation, VARIATION_COMPARISON_OFFSET); + } + private void assertNewMaintainability(int componentRef, Rating expectedVariation) { assertThat(measureRepository.getAddedRawMeasure(componentRef, NEW_MAINTAINABILITY_RATING_KEY)).hasVariation(expectedVariation.getIndex()); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/NewReliabilityAndSecurityRatingMeasuresVisitorTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/NewReliabilityAndSecurityRatingMeasuresVisitorTest.java index 89ad11a779e..51393a717e2 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/NewReliabilityAndSecurityRatingMeasuresVisitorTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/NewReliabilityAndSecurityRatingMeasuresVisitorTest.java @@ -59,12 +59,12 @@ import static org.sonar.server.computation.task.projectanalysis.component.Compon import static org.sonar.server.computation.task.projectanalysis.component.Component.Type.MODULE; import static org.sonar.server.computation.task.projectanalysis.component.Component.Type.PROJECT; import static org.sonar.server.computation.task.projectanalysis.component.ReportComponent.builder; -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; public class NewReliabilityAndSecurityRatingMeasuresVisitorTest { diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingSettingsTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingSettingsTest.java index 5bcbc3fb662..2fb023fa235 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingSettingsTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingSettingsTest.java @@ -19,14 +19,15 @@ */ package org.sonar.server.computation.task.projectanalysis.qualitymodel; -import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.sonar.api.CoreProperties; +import org.sonar.api.config.PropertyDefinitions; import org.sonar.api.config.internal.MapSettings; import org.sonar.api.measures.CoreMetrics; import org.sonar.api.utils.MessageException; +import org.sonar.core.config.CorePropertyDefinitions; import static org.assertj.core.api.Assertions.assertThat; import static org.sonar.api.CoreProperties.DEVELOPMENT_COST; @@ -36,22 +37,17 @@ import static org.sonar.api.CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS_MAN_DAYS public class RatingSettingsTest { - private MapSettings settings; + private MapSettings settings = new MapSettings(new PropertyDefinitions(CorePropertyDefinitions.all())); @Rule - public ExpectedException throwable = ExpectedException.none(); - - @Before - public void setUp() { - settings = new MapSettings(); - } + public ExpectedException expectedException = ExpectedException.none(); @Test public void load_rating_grid() { settings.setProperty(CoreProperties.RATING_GRID, "1,3.4,8,50"); RatingSettings configurationLoader = new RatingSettings(settings.asConfig()); - double[] grid = configurationLoader.getRatingGrid().getGridValues(); + double[] grid = configurationLoader.getDebtRatingGrid().getGridValues(); assertThat(grid).hasSize(4); assertThat(grid[0]).isEqualTo(1.0); assertThat(grid[1]).isEqualTo(3.4); @@ -89,12 +85,10 @@ public class RatingSettingsTest { @Test public void fail_on_invalid_rating_grid_configuration() { - RatingSettings configurationLoader = new RatingSettings(settings.asConfig()); - - throwable.expect(IllegalArgumentException.class); + expectedException.expect(IllegalArgumentException.class); settings.setProperty(CoreProperties.RATING_GRID, "a b c"); - configurationLoader.getRatingGrid(); + new RatingSettings(settings.asConfig()); } @Test @@ -117,8 +111,8 @@ public class RatingSettingsTest { settings.setProperty(LANGUAGE_SPECIFIC_PARAMETERS, "0"); settings.setProperty(LANGUAGE_SPECIFIC_PARAMETERS + "." + "0" + "." + LANGUAGE_SPECIFIC_PARAMETERS_MAN_DAYS_KEY, "40"); - throwable.expect(MessageException.class); - throwable.expectMessage("Technical debt configuration is corrupted. At least one language specific parameter has no Language key. " + + expectedException.expect(MessageException.class); + expectedException.expectMessage("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."); new RatingSettings(settings.asConfig()); diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/ReliabilityAndSecurityRatingMeasuresVisitorTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/ReliabilityAndSecurityRatingMeasuresVisitorTest.java index bc445fb7ae3..0d0ac7d8102 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/ReliabilityAndSecurityRatingMeasuresVisitorTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/ReliabilityAndSecurityRatingMeasuresVisitorTest.java @@ -60,12 +60,12 @@ import static org.sonar.server.computation.task.projectanalysis.component.Report import static org.sonar.server.computation.task.projectanalysis.measure.Measure.newMeasureBuilder; import static org.sonar.server.computation.task.projectanalysis.measure.MeasureRepoEntry.entryOf; import static org.sonar.server.computation.task.projectanalysis.measure.MeasureRepoEntry.toEntries; -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; public class ReliabilityAndSecurityRatingMeasuresVisitorTest { diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/LoadQualityGateStepTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/LoadQualityGateStepTest.java index 69a9f29b7c7..016f1c1d751 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/LoadQualityGateStepTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/LoadQualityGateStepTest.java @@ -19,7 +19,6 @@ */ package org.sonar.server.computation.task.projectanalysis.step; -import java.util.Collections; import java.util.Optional; import org.junit.Before; import org.junit.Rule; @@ -33,9 +32,8 @@ import org.sonar.server.computation.task.projectanalysis.qualitygate.QualityGate import org.sonar.server.computation.task.projectanalysis.qualitygate.QualityGateService; import org.sonar.server.qualitygate.ShortLivingBranchQualityGate; -import static java.lang.String.format; +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.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -72,37 +70,27 @@ public class LoadQualityGateStepTest { @Test public void execute_sets_default_QualityGate_when_project_has_no_settings() { when(settingsRepository.getConfiguration()).thenReturn(new MapSettings().asConfig()); - QualityGate qualityGate = mock(QualityGate.class); - when(qualityGateService.findDefaultQualityGate(any())).thenReturn(Optional.of(qualityGate)); + QualityGate defaultGate = mock(QualityGate.class); + when(qualityGateService.findDefaultQualityGate(any())).thenReturn(defaultGate); underTest.execute(); - assertThat(mutableQualityGateHolder.getQualityGate().get()).isSameAs(qualityGate); + assertThat(mutableQualityGateHolder.getQualityGate().get()).isSameAs(defaultGate); } @Test public void execute_sets_default_QualityGate_when_property_value_is_not_a_long() { expectedException.expect(IllegalStateException.class); - expectedException.expectMessage(format("Unsupported value (%s) in property sonar.qualitygate", "10 sds")); + expectedException.expectMessage("Unsupported value (10 sds) in property sonar.qualitygate"); when(settingsRepository.getConfiguration()).thenReturn(new MapSettings().setProperty("sonar.qualitygate", "10 sds").asConfig()); underTest.execute(); } - @Test - public void execute_sets_default_QualityGate_if_no_default_quality_gate_on_organization() { - when(settingsRepository.getConfiguration()).thenReturn(new MapSettings().asConfig()); - when(qualityGateService.findDefaultQualityGate(any())).thenReturn(Optional.empty()); - - underTest.execute(); - - verifyNoQualityGate(); - } - @Test public void execute_sets_QualityGate_if_it_can_be_found_by_service() { - QualityGate qualityGate = new QualityGate(10, "name", Collections.emptyList()); + QualityGate qualityGate = new QualityGate(10, "name", emptyList()); when(settingsRepository.getConfiguration()).thenReturn(new MapSettings().setProperty("sonar.qualitygate", 10).asConfig()); when(qualityGateService.findById(10)).thenReturn(Optional.of(qualityGate)); @@ -112,8 +100,4 @@ public class LoadQualityGateStepTest { assertThat(mutableQualityGateHolder.getQualityGate().get()).isSameAs(qualityGate); } - private void verifyNoQualityGate() { - assertThat(mutableQualityGateHolder.getQualityGate()).isAbsent(); - } - } diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTaskTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTaskTest.java index d92e79d0861..511e460d07d 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTaskTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTaskTest.java @@ -35,6 +35,7 @@ import org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester; import org.sonar.api.ce.posttask.Project; import org.sonar.api.ce.posttask.QualityGate; import org.sonar.api.config.Configuration; +import org.sonar.api.measures.Metric; import org.sonar.server.computation.task.projectanalysis.component.ConfigurationRepository; import org.sonar.server.qualitygate.Condition; import org.sonar.server.qualitygate.EvaluatedCondition; @@ -153,7 +154,7 @@ public class WebhookPostTaskTest { condition.isOnLeakPeriod()); webQualityGate = EvaluatedQualityGate.newBuilder() .setQualityGate(new org.sonar.server.qualitygate.QualityGate(qualityGate.getId(), qualityGate.getName(), Collections.singleton(qgCondition))) - .setStatus(EvaluatedQualityGate.Status.valueOf(qualityGate.getStatus().name())) + .setStatus(Metric.Level.valueOf(qualityGate.getStatus().name())) .addCondition(qgCondition, EvaluatedCondition.EvaluationStatus.valueOf(condition.getStatus().name()), condition.getValue()) .build(); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ActionTest.java index d7f50738fbb..d082951d143 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ActionTest.java @@ -21,49 +21,52 @@ package org.sonar.server.issue; import java.util.Collection; import java.util.Map; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.sonar.core.issue.DefaultIssue; import org.sonar.server.user.UserSession; -import static org.assertj.core.api.Assertions.assertThat; - public class ActionTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Test public void key_should_not_be_empty() { - try { - new Action("") { - @Override - public boolean verify(Map properties, Collection issues, UserSession userSession) { - return false; - } + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Action key must be set"); - @Override - public boolean execute(Map properties, Context context) { - return false; - } - }; - } catch (Exception e) { - assertThat(e).hasMessage("Action key must be set").isInstanceOf(IllegalArgumentException.class); - } + new FakeAction(""); } @Test public void key_should_not_be_null() { - try { - new Action(null) { - @Override - public boolean verify(Map properties, Collection issues, UserSession userSession) { - return false; - } + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Action key must be set"); + + new FakeAction(null); + } + + private static class FakeAction extends Action { + + FakeAction(String key) { + super(key); + } + + @Override + public boolean verify(Map properties, Collection issues, UserSession userSession) { + return false; + } + + @Override + public boolean execute(Map properties, Context context) { + return false; + } - @Override - public boolean execute(Map properties, Context context) { - return false; - } - }; - } catch (Exception e) { - assertThat(e).hasMessage("Action key must be set").isInstanceOf(IllegalArgumentException.class); + @Override + public boolean shouldRefreshMeasures() { + return false; } } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/IssueUpdaterTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/IssueUpdaterTest.java index 8eb00e90541..f9f42cfc2d6 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/IssueUpdaterTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/IssueUpdaterTest.java @@ -83,11 +83,12 @@ public class IssueUpdaterTest { private ArgumentCaptor notificationArgumentCaptor = ArgumentCaptor.forClass(IssueChangeNotification.class); private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), dbClient, new IssueIteratorFactory(dbClient)); + private TestIssueChangePostProcessor issueChangePostProcessor = new TestIssueChangePostProcessor(); private IssueUpdater underTest = new IssueUpdater(dbClient, - new ServerIssueStorage(system2, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), dbClient, issueIndexer), notificationManager); + new ServerIssueStorage(system2, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), dbClient, issueIndexer), notificationManager, issueChangePostProcessor); @Test - public void update_issue() throws Exception { + public void update_issue() { DefaultIssue issue = issueDbTester.insertIssue(newIssue().setSeverity(MAJOR)).toDefaultIssue(); IssueChangeContext context = IssueChangeContext.createUser(new Date(), "john"); issueFieldsSetter.setSeverity(issue, BLOCKER, context); @@ -99,7 +100,7 @@ public class IssueUpdaterTest { } @Test - public void verify_notification() throws Exception { + public void verify_notification() { RuleDto rule = ruleDbTester.insertRule(newRuleDto()); ComponentDto project = componentDbTester.insertPrivateProject(); ComponentDto file = componentDbTester.insertComponent(newFileDto(project)); @@ -124,7 +125,7 @@ public class IssueUpdaterTest { } @Test - public void verify_notification_on_branch() throws Exception { + public void verify_notification_on_branch() { RuleDto rule = ruleDbTester.insertRule(newRuleDto()); ComponentDto project = componentDbTester.insertMainBranch(); ComponentDto branch = componentDbTester.insertProjectBranch(project); @@ -144,7 +145,7 @@ public class IssueUpdaterTest { } @Test - public void verify_notification_when_issue_is_linked_on_removed_rule() throws Exception { + public void verify_notification_when_issue_is_linked_on_removed_rule() { RuleDto rule = ruleDbTester.insertRule(newRuleDto().setStatus(RuleStatus.REMOVED)); ComponentDto project = componentDbTester.insertPrivateProject(); ComponentDto file = componentDbTester.insertComponent(newFileDto(project)); @@ -175,7 +176,7 @@ public class IssueUpdaterTest { IssueChangeContext context = IssueChangeContext.createUser(new Date(), "john"); issueFieldsSetter.setSeverity(issue, BLOCKER, context); - SearchResponseData preloadedSearchResponseData = underTest.saveIssueAndPreloadSearchResponseData(dbTester.getSession(), issue, context, null); + SearchResponseData preloadedSearchResponseData = underTest.saveIssueAndPreloadSearchResponseData(dbTester.getSession(), issue, context, null, true); assertThat(preloadedSearchResponseData.getIssues()) .hasSize(1); @@ -187,6 +188,7 @@ public class IssueUpdaterTest { assertThat(preloadedSearchResponseData.getComponents()) .extracting(ComponentDto::uuid) .containsOnly(project.uuid(), file.uuid()); + assertThat(issueChangePostProcessor.calledComponents()).containsExactlyInAnyOrder(file); } @Test @@ -199,7 +201,7 @@ public class IssueUpdaterTest { IssueChangeContext context = IssueChangeContext.createUser(new Date(), "john"); issueFieldsSetter.setSeverity(issue, BLOCKER, context); - SearchResponseData preloadedSearchResponseData = underTest.saveIssueAndPreloadSearchResponseData(dbTester.getSession(), issue, context, null); + SearchResponseData preloadedSearchResponseData = underTest.saveIssueAndPreloadSearchResponseData(dbTester.getSession(), issue, context, null, false); assertThat(preloadedSearchResponseData.getIssues()) .hasSize(1); diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/TestIssueChangePostProcessor.java b/server/sonar-server/src/test/java/org/sonar/server/issue/TestIssueChangePostProcessor.java new file mode 100644 index 00000000000..e18cf411848 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/TestIssueChangePostProcessor.java @@ -0,0 +1,47 @@ +/* + * 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.ArrayList; +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; + +public class TestIssueChangePostProcessor implements IssueChangePostProcessor { + + private boolean called = false; + private final List calledComponents = new ArrayList<>(); + + @Override + public void process(DbSession dbSession, List changedIssues, Collection components) { + called = true; + calledComponents.addAll(components); + } + + public boolean wasCalled() { + return called; + } + + public List calledComponents() { + return calledComponents; + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AddCommentActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AddCommentActionTest.java index d731f926115..67b52acfd0c 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AddCommentActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AddCommentActionTest.java @@ -45,6 +45,7 @@ import org.sonar.server.issue.IssueFieldsSetter; import org.sonar.server.issue.IssueFinder; import org.sonar.server.issue.IssueUpdater; import org.sonar.server.issue.ServerIssueStorage; +import org.sonar.server.issue.TestIssueChangePostProcessor; import org.sonar.server.issue.index.IssueIndexDefinition; import org.sonar.server.issue.index.IssueIndexer; import org.sonar.server.issue.index.IssueIteratorFactory; @@ -94,7 +95,8 @@ public class AddCommentActionTest { private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), dbClient, new IssueIteratorFactory(dbClient)); private ServerIssueStorage serverIssueStorage = new ServerIssueStorage(system2, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), dbClient, issueIndexer); - private IssueUpdater issueUpdater = new IssueUpdater(dbClient, serverIssueStorage, mock(NotificationManager.class)); + private TestIssueChangePostProcessor issueChangePostProcessor = new TestIssueChangePostProcessor(); + private IssueUpdater issueUpdater = new IssueUpdater(dbClient, serverIssueStorage, mock(NotificationManager.class), issueChangePostProcessor); private OperationResponseWriter responseWriter = mock(OperationResponseWriter.class); private ArgumentCaptor preloadedSearchResponseDataCaptor = ArgumentCaptor.forClass(SearchResponseData.class); @@ -107,7 +109,7 @@ public class AddCommentActionTest { } @Test - public void add_comment() throws Exception { + public void add_comment() { IssueDto issueDto = issueDbTester.insertIssue(); loginWithBrowsePermission(issueDto, USER); @@ -128,10 +130,11 @@ public class AddCommentActionTest { IssueDto issueReloaded = dbClient.issueDao().selectByKey(dbTester.getSession(), issueDto.getKey()).get(); assertThat(issueReloaded.getIssueUpdateTime()).isEqualTo(NOW); + assertThat(issueChangePostProcessor.wasCalled()).isFalse(); } @Test - public void fail_when_missing_issue_key() throws Exception { + public void fail_when_missing_issue_key() { userSession.logIn("john"); expectedException.expect(IllegalArgumentException.class); @@ -139,7 +142,7 @@ public class AddCommentActionTest { } @Test - public void fail_when_issue_does_not_exist() throws Exception { + public void fail_when_issue_does_not_exist() { userSession.logIn("john"); expectedException.expect(NotFoundException.class); @@ -147,7 +150,7 @@ public class AddCommentActionTest { } @Test - public void fail_when_missing_comment_text() throws Exception { + public void fail_when_missing_comment_text() { userSession.logIn("john"); expectedException.expect(IllegalArgumentException.class); @@ -155,7 +158,7 @@ public class AddCommentActionTest { } @Test - public void fail_when_empty_comment_text() throws Exception { + public void fail_when_empty_comment_text() { IssueDto issueDto = issueDbTester.insertIssue(); loginWithBrowsePermission(issueDto, USER); @@ -164,13 +167,13 @@ public class AddCommentActionTest { } @Test - public void fail_when_not_authenticated() throws Exception { + public void fail_when_not_authenticated() { expectedException.expect(UnauthorizedException.class); call("ABCD", "please fix it"); } @Test - public void fail_when_not_enough_permission() throws Exception { + public void fail_when_not_enough_permission() { IssueDto issueDto = issueDbTester.insertIssue(); loginWithBrowsePermission(issueDto, CODEVIEWER); diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AssignActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AssignActionTest.java index 6def1097474..aed5de8eb82 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AssignActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AssignActionTest.java @@ -42,6 +42,7 @@ import org.sonar.server.issue.IssueFieldsSetter; import org.sonar.server.issue.IssueFinder; import org.sonar.server.issue.IssueUpdater; import org.sonar.server.issue.ServerIssueStorage; +import org.sonar.server.issue.TestIssueChangePostProcessor; import org.sonar.server.issue.index.IssueIndexDefinition; import org.sonar.server.issue.index.IssueIndexer; import org.sonar.server.issue.index.IssueIteratorFactory; @@ -82,16 +83,17 @@ public class AssignActionTest { private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db); private IssueIndexer issueIndexer = new IssueIndexer(es.client(), db.getDbClient(), new IssueIteratorFactory(db.getDbClient())); private OperationResponseWriter responseWriter = mock(OperationResponseWriter.class); + private TestIssueChangePostProcessor issueChangePostProcessor = new TestIssueChangePostProcessor(); private AssignAction underTest = new AssignAction(system2, userSession, db.getDbClient(), new IssueFinder(db.getDbClient(), userSession), new IssueFieldsSetter(), new IssueUpdater(db.getDbClient(), new ServerIssueStorage(system2, new DefaultRuleFinder(db.getDbClient(), defaultOrganizationProvider), db.getDbClient(), issueIndexer), - mock(NotificationManager.class)), + mock(NotificationManager.class), issueChangePostProcessor), responseWriter); private ArgumentCaptor preloadedSearchResponseDataCaptor = ArgumentCaptor.forClass(SearchResponseData.class); private WsActionTester ws = new WsActionTester(underTest); @Test - public void assign_to_someone() throws Exception { + public void assign_to_someone() { IssueDto issue = newIssueWithBrowsePermission(); insertUser("arthur"); @@ -103,10 +105,11 @@ public class AssignActionTest { checkIssueAssignee(issue.getKey(), "arthur"); verify(responseWriter).write(eq(issue.getKey()), preloadedSearchResponseDataCaptor.capture(), any(Request.class), any(Response.class)); verifyContentOfPreloadedSearchResponseData(issue); + assertThat(issueChangePostProcessor.wasCalled()).isFalse(); } @Test - public void assign_to_me() throws Exception { + public void assign_to_me() { IssueDto issue = newIssueWithBrowsePermission(); ws.newRequest() @@ -117,10 +120,11 @@ public class AssignActionTest { checkIssueAssignee(issue.getKey(), CURRENT_USER_LOGIN); verify(responseWriter).write(eq(issue.getKey()), preloadedSearchResponseDataCaptor.capture(), any(Request.class), any(Response.class)); verifyContentOfPreloadedSearchResponseData(issue); + assertThat(issueChangePostProcessor.wasCalled()).isFalse(); } @Test - public void assign_to_me_using_deprecated_me_param() throws Exception { + public void assign_to_me_using_deprecated_me_param() { IssueDto issue = newIssueWithBrowsePermission(); ws.newRequest() @@ -134,7 +138,7 @@ public class AssignActionTest { } @Test - public void unassign() throws Exception { + public void unassign() { IssueDto issue = newIssueWithBrowsePermission(); ws.newRequest() @@ -144,10 +148,11 @@ public class AssignActionTest { checkIssueAssignee(issue.getKey(), null); verify(responseWriter).write(eq(issue.getKey()), preloadedSearchResponseDataCaptor.capture(), any(Request.class), any(Response.class)); verifyContentOfPreloadedSearchResponseData(issue); + assertThat(issueChangePostProcessor.wasCalled()).isFalse(); } @Test - public void unassign_with_empty_assignee_param() throws Exception { + public void unassign_with_empty_assignee_param() { IssueDto issue = newIssueWithBrowsePermission(); ws.newRequest() @@ -161,7 +166,7 @@ public class AssignActionTest { } @Test - public void nothing_to_do_when_new_assignee_is_same_as_old_one() throws Exception { + public void nothing_to_do_when_new_assignee_is_same_as_old_one() { IssueDto issue = newIssueWithBrowsePermission(); insertUser(PREVIOUS_ASSIGNEE); @@ -177,7 +182,7 @@ public class AssignActionTest { } @Test - public void fail_when_assignee_does_not_exist() throws Exception { + public void fail_when_assignee_does_not_exist() { IssueDto issue = newIssueWithBrowsePermission(); expectedException.expect(NotFoundException.class); @@ -189,7 +194,7 @@ public class AssignActionTest { } @Test - public void fail_when_assignee_is_disabled() throws Exception { + public void fail_when_assignee_is_disabled() { IssueDto issue = newIssueWithBrowsePermission(); db.users().insertUser(user -> user.setActive(false)); @@ -202,7 +207,7 @@ public class AssignActionTest { } @Test - public void fail_when_not_authenticated() throws Exception { + public void fail_when_not_authenticated() { IssueDto issue = newIssue(); userSession.anonymous(); @@ -215,7 +220,7 @@ public class AssignActionTest { } @Test - public void fail_when_missing_browse_permission() throws Exception { + public void fail_when_missing_browse_permission() { IssueDto issue = newIssue(); setUserWithPermission(issue, CODEVIEWER); @@ -228,7 +233,7 @@ public class AssignActionTest { } @Test - public void fail_when_assignee_is_not_member_of_organization_of_project_issue() throws Exception { + public void fail_when_assignee_is_not_member_of_organization_of_project_issue() { OrganizationDto org = db.organizations().insert(organizationDto -> organizationDto.setKey("Organization key")); IssueDto issueDto = db.issues().insertIssue(org); setUserWithBrowsePermission(issueDto); diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/BulkChangeActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/BulkChangeActionTest.java index e75761f7351..764ebe56070 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/BulkChangeActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/BulkChangeActionTest.java @@ -20,9 +20,6 @@ package org.sonar.server.issue.ws; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -37,8 +34,6 @@ import org.sonar.api.config.internal.MapSettings; import org.sonar.api.rules.RuleType; import org.sonar.api.server.ws.WebService; import org.sonar.api.utils.System2; -import org.sonar.core.issue.DefaultIssue; -import org.sonar.core.issue.IssueChangeContext; import org.sonar.db.DbClient; import org.sonar.db.DbTester; import org.sonar.db.component.ComponentDto; @@ -53,6 +48,7 @@ import org.sonar.server.issue.Action; import org.sonar.server.issue.IssueFieldsSetter; import org.sonar.server.issue.IssueStorage; import org.sonar.server.issue.ServerIssueStorage; +import org.sonar.server.issue.TestIssueChangePostProcessor; import org.sonar.server.issue.TransitionService; import org.sonar.server.issue.index.IssueIndexDefinition; import org.sonar.server.issue.index.IssueIndexer; @@ -63,9 +59,6 @@ import org.sonar.server.issue.workflow.IssueWorkflow; import org.sonar.server.notification.NotificationManager; import org.sonar.server.organization.DefaultOrganizationProvider; import org.sonar.server.organization.TestDefaultOrganizationProvider; -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.rule.DefaultRuleFinder; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.TestRequest; @@ -79,12 +72,8 @@ import static java.util.Collections.singletonList; import static java.util.Objects.requireNonNull; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Matchers.same; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static org.sonar.api.issue.Issue.RESOLUTION_FIXED; import static org.sonar.api.issue.Issue.STATUS_CLOSED; @@ -92,7 +81,6 @@ import static org.sonar.api.issue.Issue.STATUS_OPEN; 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.CODE_SMELL; import static org.sonar.api.rules.RuleType.VULNERABILITY; import static org.sonar.api.web.UserRole.ISSUE_ADMIN; import static org.sonar.api.web.UserRole.USER; @@ -125,8 +113,7 @@ public class BulkChangeActionTest { private IssueStorage issueStorage = new ServerIssueStorage(system2, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), dbClient, new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient))); private NotificationManager notificationManager = mock(NotificationManager.class); - private QGChangeEventFactory qgChangeEventFactory = mock(QGChangeEventFactory.class); - private QGChangeEventListeners qgChangeEventListeners = mock(QGChangeEventListeners.class); + private TestIssueChangePostProcessor issueChangePostProcessor = new TestIssueChangePostProcessor(); private List actions = new ArrayList<>(); private RuleDto rule; @@ -135,8 +122,7 @@ public class BulkChangeActionTest { private ComponentDto file; private UserDto user; - private WsActionTester tester = new WsActionTester( - new BulkChangeAction(system2, userSession, dbClient, issueStorage, notificationManager, actions, qgChangeEventFactory, qgChangeEventListeners)); + private WsActionTester tester = new WsActionTester(new BulkChangeAction(system2, userSession, dbClient, issueStorage, notificationManager, actions, issueChangePostProcessor)); @Before public void setUp() { @@ -154,7 +140,6 @@ public class BulkChangeActionTest { public void set_type() { setUserProjectPermissions(USER, ISSUE_ADMIN); IssueDto issueDto = db.issues().insertIssue(newUnresolvedIssue().setType(BUG)); - List qgChangeEvents = mockQGChangeEvents(CODE_SMELL, null); BulkChangeWsResponse response = call(builder() .setIssues(singletonList(issueDto.getKey())) @@ -166,8 +151,7 @@ public class BulkChangeActionTest { assertThat(reloaded.getType()).isEqualTo(RuleType.CODE_SMELL.getDbConstant()); assertThat(reloaded.getUpdatedAt()).isEqualTo(NOW); - QGChangeEventFactory.IssueChangeData issueChangeData = verifyIssueChangeData(CODE_SMELL, null, new String[] {file.uuid()}, issueDto); - verifyBroadcastOnIssueChange(issueChangeData, qgChangeEvents); + verifyPostProcessorCalled(file); } @Test @@ -185,7 +169,7 @@ public class BulkChangeActionTest { assertThat(reloaded.getSeverity()).isEqualTo(MINOR); assertThat(reloaded.getUpdatedAt()).isEqualTo(NOW); - verifyZeroInteractions(qgChangeEventFactory); + verifyPostProcessorCalled(file); } @Test @@ -203,7 +187,8 @@ public class BulkChangeActionTest { assertThat(reloaded.getTags()).containsOnly("tag1", "tag2", "tag3"); assertThat(reloaded.getUpdatedAt()).isEqualTo(NOW); - verifyZeroInteractions(qgChangeEventFactory); + // no need to refresh measures + verifyPostProcessorNotCalled(); } @Test @@ -221,14 +206,14 @@ public class BulkChangeActionTest { assertThat(reloaded.getAssignee()).isNull(); assertThat(reloaded.getUpdatedAt()).isEqualTo(NOW); - verifyZeroInteractions(qgChangeEventFactory); + // no need to refresh measures + verifyPostProcessorNotCalled(); } @Test public void bulk_change_with_comment() { setUserProjectPermissions(USER); IssueDto issueDto = db.issues().insertIssue(newUnresolvedIssue().setType(BUG)); - List qgChangeEvents = mockQGChangeEvents(null, "confirm"); BulkChangeWsResponse response = call(builder() .setIssues(singletonList(issueDto.getKey())) @@ -241,8 +226,7 @@ public class BulkChangeActionTest { assertThat(issueComment.getUserLogin()).isEqualTo("john"); assertThat(issueComment.getChangeData()).isEqualTo("type was badly defined"); - QGChangeEventFactory.IssueChangeData issueChangeData = verifyIssueChangeData(null, "confirm", new String[] {file.uuid()}, issueDto); - verifyBroadcastOnIssueChange(issueChangeData, qgChangeEvents); + verifyPostProcessorCalled(file); } @Test @@ -254,7 +238,6 @@ public class BulkChangeActionTest { IssueDto issue1 = db.issues().insertIssue(newUnresolvedIssue().setAssignee(user.getLogin())).setType(BUG).setSeverity(MINOR); IssueDto issue2 = db.issues().insertIssue(newUnresolvedIssue().setAssignee(userToAssign.getLogin())).setType(BUG).setSeverity(MAJOR); IssueDto issue3 = db.issues().insertIssue(newUnresolvedIssue().setAssignee(null)).setType(VULNERABILITY).setSeverity(MAJOR); - List qgChangeEvents = mockQGChangeEvents(VULNERABILITY, null); BulkChangeWsResponse response = call(builder() .setIssues(asList(issue1.getKey(), issue2.getKey(), issue3.getKey())) @@ -271,15 +254,13 @@ public class BulkChangeActionTest { tuple(issue2.getKey(), userToAssign.getLogin(), VULNERABILITY.getDbConstant(), MINOR, NOW), tuple(issue3.getKey(), userToAssign.getLogin(), VULNERABILITY.getDbConstant(), MINOR, NOW)); - QGChangeEventFactory.IssueChangeData issueChangeData = verifyIssueChangeData(VULNERABILITY, null, new String[] {file.uuid()}, issue1, issue2, issue3); - verifyBroadcastOnIssueChange(issueChangeData, qgChangeEvents); + verifyPostProcessorCalled(file); } @Test public void send_notification() { setUserProjectPermissions(USER); IssueDto issueDto = db.issues().insertIssue(newUnresolvedIssue().setType(BUG)); - List qgChangeEvents = mockQGChangeEvents(null, "confirm"); BulkChangeWsResponse response = call(builder() .setIssues(singletonList(issueDto.getKey())) @@ -298,9 +279,6 @@ public class BulkChangeActionTest { assertThat(issueChangeNotificationCaptor.getValue().getFieldValue("ruleName")).isEqualTo(rule.getName()); assertThat(issueChangeNotificationCaptor.getValue().getFieldValue("changeAuthor")).isEqualTo(user.getLogin()); assertThat(issueChangeNotificationCaptor.getValue().getFieldValue("branch")).isNull(); - - QGChangeEventFactory.IssueChangeData issueChangeData = verifyIssueChangeData(null, "confirm", new String[] {file.uuid()}, issueDto); - verifyBroadcastOnIssueChange(issueChangeData, qgChangeEvents); } @Test @@ -311,7 +289,6 @@ public class BulkChangeActionTest { ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setKey(branchName)); ComponentDto fileOnBranch = db.components().insertComponent(newFileDto(branch)); IssueDto issueDto = db.issues().insertIssue(newUnresolvedIssue(rule, fileOnBranch, branch).setType(BUG)); - List qgChangeEvents = mockQGChangeEvents(null, "confirm"); BulkChangeWsResponse response = call(builder() .setIssues(singletonList(issueDto.getKey())) @@ -331,8 +308,7 @@ public class BulkChangeActionTest { assertThat(issueChangeNotificationCaptor.getValue().getFieldValue("changeAuthor")).isEqualTo(user.getLogin()); assertThat(issueChangeNotificationCaptor.getValue().getFieldValue("branch")).isEqualTo(branchName); - QGChangeEventFactory.IssueChangeData issueChangeData = verifyIssueChangeData(null, "confirm", new String[] {fileOnBranch.uuid()}, issueDto); - verifyBroadcastOnIssueChange(issueChangeData, qgChangeEvents); + verifyPostProcessorCalled(fileOnBranch); } @Test @@ -342,7 +318,6 @@ public class BulkChangeActionTest { IssueDto issue2 = db.issues().insertIssue(newUnresolvedIssue().setType(BUG)); IssueDto issue3 = db.issues().insertIssue(newUnresolvedIssue().setType(VULNERABILITY)); ArgumentCaptor issueChangeNotificationCaptor = ArgumentCaptor.forClass(IssueChangeNotification.class); - List qgChangeEvents = mockQGChangeEvents(BUG, null); BulkChangeWsResponse response = call(builder() .setIssues(asList(issue1.getKey(), issue2.getKey(), issue3.getKey())) @@ -355,18 +330,17 @@ public class BulkChangeActionTest { assertThat(issueChangeNotificationCaptor.getAllValues()).hasSize(1); assertThat(issueChangeNotificationCaptor.getValue().getFieldValue("key")).isEqualTo(issue3.getKey()); - QGChangeEventFactory.IssueChangeData issueChangeData = verifyIssueChangeData(BUG, null, new String[] {file.uuid()}, issue3); - verifyBroadcastOnIssueChange(issueChangeData, qgChangeEvents); + verifyPostProcessorCalled(file); } @Test - public void ignore_issues_when_condition_does_not_match() { + public void ignore_the_issues_that_do_not_match_conditions() { setUserProjectPermissions(USER, ISSUE_ADMIN); + ComponentDto file2 = db.components().insertComponent(newFileDto(project)); IssueDto issue1 = db.issues().insertIssue(newUnresolvedIssue().setType(BUG)); // These 2 issues will be ignored as they are resolved, changing type is not possible IssueDto issue2 = db.issues().insertIssue(newResolvedIssue().setType(BUG)); - IssueDto issue3 = db.issues().insertIssue(newResolvedIssue().setType(BUG)); - List qgChangeEvents = mockQGChangeEvents(VULNERABILITY, null); + IssueDto issue3 = db.issues().insertIssue(newResolvedIssue().setType(BUG).setComponent(file2)); BulkChangeWsResponse response = call(builder() .setIssues(asList(issue1.getKey(), issue2.getKey(), issue3.getKey())) @@ -381,18 +355,18 @@ public class BulkChangeActionTest { tuple(issue3.getKey(), BUG.getDbConstant(), issue2.getUpdatedAt()), tuple(issue2.getKey(), BUG.getDbConstant(), issue3.getUpdatedAt())); - QGChangeEventFactory.IssueChangeData issueChangeData = verifyIssueChangeData(VULNERABILITY, null, new String[] {file.uuid()}, issue1); - verifyBroadcastOnIssueChange(issueChangeData, qgChangeEvents); + // file2 is not refreshed + verifyPostProcessorCalled(file); } @Test public void ignore_issues_when_there_is_nothing_to_do() { setUserProjectPermissions(USER, ISSUE_ADMIN); + ComponentDto file2 = db.components().insertComponent(newFileDto(project)); IssueDto issue1 = db.issues().insertIssue(newUnresolvedIssue().setType(BUG).setSeverity(MINOR)); // These 2 issues will be ignored as there's nothing to do IssueDto issue2 = db.issues().insertIssue(newUnresolvedIssue().setType(VULNERABILITY)); - IssueDto issue3 = db.issues().insertIssue(newUnresolvedIssue().setType(VULNERABILITY)); - List qgChangeEvents = mockQGChangeEvents(VULNERABILITY, null); + IssueDto issue3 = db.issues().insertIssue(newUnresolvedIssue().setType(VULNERABILITY).setComponent(file2)); BulkChangeWsResponse response = call(builder() .setIssues(asList(issue1.getKey(), issue2.getKey(), issue3.getKey())) @@ -407,8 +381,8 @@ public class BulkChangeActionTest { tuple(issue2.getKey(), VULNERABILITY.getDbConstant(), issue2.getUpdatedAt()), tuple(issue3.getKey(), VULNERABILITY.getDbConstant(), issue3.getUpdatedAt())); - QGChangeEventFactory.IssueChangeData issueChangeData = verifyIssueChangeData(VULNERABILITY, null, new String[] {file.uuid()}, issue1); - verifyBroadcastOnIssueChange(issueChangeData, qgChangeEvents); + // file2 is not refreshed + verifyPostProcessorCalled(file); } @Test @@ -418,7 +392,6 @@ public class BulkChangeActionTest { // These 2 issues will be ignored as there's nothing to do IssueDto issue2 = db.issues().insertIssue(newUnresolvedIssue().setType(VULNERABILITY)); IssueDto issue3 = db.issues().insertIssue(newUnresolvedIssue().setType(VULNERABILITY)); - List qgChangeEvents = mockQGChangeEvents(VULNERABILITY, null); BulkChangeWsResponse response = call(builder() .setIssues(asList(issue1.getKey(), issue2.getKey(), issue3.getKey())) @@ -431,8 +404,7 @@ public class BulkChangeActionTest { assertThat(dbClient.issueChangeDao().selectByTypeAndIssueKeys(db.getSession(), singletonList(issue2.getKey()), TYPE_COMMENT)).isEmpty(); assertThat(dbClient.issueChangeDao().selectByTypeAndIssueKeys(db.getSession(), singletonList(issue3.getKey()), TYPE_COMMENT)).isEmpty(); - QGChangeEventFactory.IssueChangeData issueChangeData = verifyIssueChangeData(VULNERABILITY, null, new String[] {file.uuid()}, issue1); - verifyBroadcastOnIssueChange(issueChangeData, qgChangeEvents); + verifyPostProcessorCalled(file); } @Test @@ -444,7 +416,6 @@ public class BulkChangeActionTest { // User has not browse permission on these 2 issues IssueDto notAuthorizedIssue1 = db.issues().insertIssue(newUnresolvedIssue(rule, anotherFile, anotherProject).setType(BUG)); IssueDto notAuthorizedIssue2 = db.issues().insertIssue(newUnresolvedIssue(rule, anotherFile, anotherProject).setType(BUG)); - List qgChangeEvents = mockQGChangeEvents(VULNERABILITY, null); BulkChangeWsResponse response = call(builder() .setIssues(asList(authorizedIssue.getKey(), notAuthorizedIssue1.getKey(), notAuthorizedIssue2.getKey())) @@ -459,8 +430,7 @@ public class BulkChangeActionTest { tuple(notAuthorizedIssue1.getKey(), BUG.getDbConstant(), notAuthorizedIssue1.getUpdatedAt()), tuple(notAuthorizedIssue2.getKey(), BUG.getDbConstant(), notAuthorizedIssue2.getUpdatedAt())); - QGChangeEventFactory.IssueChangeData issueChangeData = verifyIssueChangeData(VULNERABILITY, null, new String[] {file.uuid()}, authorizedIssue); - verifyBroadcastOnIssueChange(issueChangeData, qgChangeEvents); + verifyPostProcessorCalled(file); } @Test @@ -487,6 +457,7 @@ public class BulkChangeActionTest { tuple(authorizedIssue1.getKey(), VULNERABILITY.getDbConstant(), NOW), tuple(notAuthorizedIssue1.getKey(), BUG.getDbConstant(), notAuthorizedIssue1.getUpdatedAt()), tuple(notAuthorizedIssue2.getKey(), BUG.getDbConstant(), notAuthorizedIssue2.getUpdatedAt())); + verifyPostProcessorCalled(file); } @Test @@ -513,12 +484,15 @@ public class BulkChangeActionTest { tuple(authorizedIssue1.getKey(), MINOR, NOW), tuple(notAuthorizedIssue1.getKey(), MAJOR, notAuthorizedIssue1.getUpdatedAt()), tuple(notAuthorizedIssue2.getKey(), MAJOR, notAuthorizedIssue2.getUpdatedAt())); + + verifyPostProcessorCalled(file); } @Test public void fail_when_only_comment_action() { setUserProjectPermissions(USER); IssueDto issueDto = db.issues().insertIssue(newUnresolvedIssue().setType(BUG)); + expectedException.expectMessage("At least one action must be provided"); expectedException.expect(IllegalArgumentException.class); @@ -531,6 +505,7 @@ public class BulkChangeActionTest { @Test public void fail_when_number_of_issues_is_more_than_500() { userSession.logIn("john"); + expectedException.expectMessage("Number of issues is limited to 500"); expectedException.expect(IllegalArgumentException.class); @@ -557,60 +532,6 @@ public class BulkChangeActionTest { assertThat(action.responseExample()).isNotNull(); } - private void verifyIssueChangeWebhookCalled(@Nullable RuleType expectedRuleType, @Nullable String transitionKey, - String[] componentUUids, - IssueDto... issueDtos) { - QGChangeEventFactory.IssueChange issueChange = new QGChangeEventFactory.IssueChange(expectedRuleType, transitionKey); - IssueChangeContext user = IssueChangeContext.createUser(new Date(NOW), userSession.getLogin()); - List changeEvents = Collections.singletonList(mock(QGChangeEvent.class)); - when(qgChangeEventFactory.from(any(QGChangeEventFactory.IssueChangeData.class), eq(issueChange), eq(user))).thenReturn(changeEvents); - - ArgumentCaptor issueChangeDataCaptor = ArgumentCaptor.forClass(QGChangeEventFactory.IssueChangeData.class); - verify(qgChangeEventFactory).from(issueChangeDataCaptor.capture(), eq(issueChange), eq(user)); - - QGChangeEventFactory.IssueChangeData issueChangeData = issueChangeDataCaptor.getValue(); - assertThat(issueChangeData.getIssues()) - .extracting(DefaultIssue::key) - .containsOnly(Arrays.stream(issueDtos).map(IssueDto::getKey).toArray(String[]::new)); - assertThat(issueChangeData.getComponents()) - .extracting(ComponentDto::uuid) - .containsOnly(componentUUids); - - verify(qgChangeEventListeners).broadcastOnIssueChange(same(issueChangeData), same(changeEvents)); - } - - private List mockQGChangeEvents(@Nullable RuleType expectedRuleType, @Nullable String transitionKey) { - QGChangeEventFactory.IssueChange issueChange = new QGChangeEventFactory.IssueChange(expectedRuleType, transitionKey); - IssueChangeContext user = IssueChangeContext.createUser(new Date(NOW), userSession.getLogin()); - List changeEvents = Collections.singletonList(mock(QGChangeEvent.class)); - when(qgChangeEventFactory.from(any(QGChangeEventFactory.IssueChangeData.class), eq(issueChange), eq(user))).thenReturn(changeEvents); - - return changeEvents; - } - - private QGChangeEventFactory.IssueChangeData verifyIssueChangeData(@Nullable RuleType expectedRuleType, @Nullable String transitionKey, - String[] componentUUids, - IssueDto... issueDtos) { - QGChangeEventFactory.IssueChange issueChange = new QGChangeEventFactory.IssueChange(expectedRuleType, transitionKey); - IssueChangeContext user = IssueChangeContext.createUser(new Date(NOW), userSession.getLogin()); - - ArgumentCaptor issueChangeDataCaptor = ArgumentCaptor.forClass(QGChangeEventFactory.IssueChangeData.class); - verify(qgChangeEventFactory).from(issueChangeDataCaptor.capture(), eq(issueChange), eq(user)); - - QGChangeEventFactory.IssueChangeData issueChangeData = issueChangeDataCaptor.getValue(); - assertThat(issueChangeData.getIssues()) - .extracting(DefaultIssue::key) - .containsOnly(Arrays.stream(issueDtos).map(IssueDto::getKey).toArray(String[]::new)); - assertThat(issueChangeData.getComponents()) - .extracting(ComponentDto::uuid) - .containsOnly(componentUUids); - return issueChangeData; - } - - private void verifyBroadcastOnIssueChange(QGChangeEventFactory.IssueChangeData issueChangeData, List changeEvents) { - verify(qgChangeEventListeners).broadcastOnIssueChange(same(issueChangeData), same(changeEvents)); - } - private BulkChangeWsResponse call(BulkChangeRequest bulkChangeRequest) { TestRequest request = tester.newRequest(); setNullable(bulkChangeRequest.getIssues(), value -> request.setParam("issues", String.join(",", value))); @@ -652,6 +573,14 @@ public class BulkChangeActionTest { return db.getDbClient().issueDao().selectByKeys(db.getSession(), asList(issueKeys)); } + private void verifyPostProcessorCalled(ComponentDto... components) { + assertThat(issueChangePostProcessor.calledComponents()).containsExactlyInAnyOrder(components); + } + + private void verifyPostProcessorNotCalled() { + assertThat(issueChangePostProcessor.wasCalled()).isFalse(); + } + private IssueDto newUnresolvedIssue(RuleDto rule, ComponentDto file, ComponentDto project) { return newDto(rule, file, project).setStatus(STATUS_OPEN).setResolution(null); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/DoTransitionActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/DoTransitionActionTest.java index 42b9de08966..d3c69b8d696 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/DoTransitionActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/DoTransitionActionTest.java @@ -19,9 +19,6 @@ */ package org.sonar.server.issue.ws; -import java.util.Collections; -import java.util.Date; -import java.util.List; import javax.annotation.Nullable; import org.junit.Before; import org.junit.Rule; @@ -32,8 +29,6 @@ import org.sonar.api.config.internal.MapSettings; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.utils.System2; -import org.sonar.core.issue.DefaultIssue; -import org.sonar.core.issue.IssueChangeContext; import org.sonar.db.DbClient; import org.sonar.db.DbTester; import org.sonar.db.component.ComponentDbTester; @@ -51,6 +46,7 @@ import org.sonar.server.issue.IssueFieldsSetter; import org.sonar.server.issue.IssueFinder; import org.sonar.server.issue.IssueUpdater; import org.sonar.server.issue.ServerIssueStorage; +import org.sonar.server.issue.TestIssueChangePostProcessor; import org.sonar.server.issue.TransitionService; import org.sonar.server.issue.index.IssueIndexDefinition; import org.sonar.server.issue.index.IssueIndexer; @@ -60,9 +56,6 @@ import org.sonar.server.issue.workflow.IssueWorkflow; import org.sonar.server.notification.NotificationManager; import org.sonar.server.organization.DefaultOrganizationProvider; import org.sonar.server.organization.TestDefaultOrganizationProvider; -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.rule.DefaultRuleFinder; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.TestRequest; @@ -113,16 +106,15 @@ public class DoTransitionActionTest { private TransitionService transitionService = new TransitionService(userSession, workflow); private OperationResponseWriter responseWriter = mock(OperationResponseWriter.class); private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), dbClient, new IssueIteratorFactory(dbClient)); + private TestIssueChangePostProcessor issueChangePostProcessor = new TestIssueChangePostProcessor(); private IssueUpdater issueUpdater = new IssueUpdater(dbClient, - new ServerIssueStorage(system2, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), dbClient, issueIndexer), mock(NotificationManager.class)); + new ServerIssueStorage(system2, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), dbClient, issueIndexer), mock(NotificationManager.class), + issueChangePostProcessor); private ComponentDto project; private ComponentDto file; private ArgumentCaptor preloadedSearchResponseDataCaptor = ArgumentCaptor.forClass(SearchResponseData.class); - private QGChangeEventFactory qgChangeEventFactory = mock(QGChangeEventFactory.class); - private QGChangeEventListeners qgChangeEventListeners = mock(QGChangeEventListeners.class); - private WsAction underTest = new DoTransitionAction(dbClient, userSession, new IssueFinder(dbClient, userSession), issueUpdater, transitionService, responseWriter, system2, - qgChangeEventFactory, qgChangeEventListeners); + private WsAction underTest = new DoTransitionAction(dbClient, userSession, new IssueFinder(dbClient, userSession), issueUpdater, transitionService, responseWriter, system2); private WsActionTester tester = new WsActionTester(underTest); @Before @@ -137,11 +129,6 @@ public class DoTransitionActionTest { when(system2.now()).thenReturn(NOW); IssueDto issueDto = issueDbTester.insertIssue(newIssue().setStatus(STATUS_OPEN).setResolution(null)); userSession.logIn("john").addProjectPermission(USER, project, file); - IssueChangeContext changeContext = IssueChangeContext.createUser(new Date(NOW), userSession.getLogin()); - QGChangeEventFactory.IssueChange issueChange = new QGChangeEventFactory.IssueChange(null, "confirm"); - List qgChangeEvents = Collections.singletonList(mock(QGChangeEvent.class)); - when(qgChangeEventFactory.from(any(QGChangeEventFactory.IssueChangeData.class), eq(issueChange), eq(changeContext))) - .thenReturn(qgChangeEvents); call(issueDto.getKey(), "confirm"); @@ -149,17 +136,7 @@ public class DoTransitionActionTest { verifyContentOfPreloadedSearchResponseData(issueDto); IssueDto issueReloaded = dbClient.issueDao().selectByKey(dbTester.getSession(), issueDto.getKey()).get(); assertThat(issueReloaded.getStatus()).isEqualTo(STATUS_CONFIRMED); - - ArgumentCaptor issueChangeDataCaptor = ArgumentCaptor.forClass(QGChangeEventFactory.IssueChangeData.class); - verify(qgChangeEventFactory).from(issueChangeDataCaptor.capture(), eq(issueChange), eq(changeContext)); - QGChangeEventFactory.IssueChangeData issueChangeData = issueChangeDataCaptor.getValue(); - assertThat(issueChangeData.getIssues()) - .extracting(DefaultIssue::key) - .containsOnly(issueDto.getKey()); - assertThat(issueChangeData.getComponents()) - .extracting(ComponentDto::uuid) - .containsOnly(issueDto.getComponentUuid(), issueDto.getProjectUuid()); - verify(qgChangeEventListeners).broadcastOnIssueChange(issueChangeData, qgChangeEvents); + assertThat(issueChangePostProcessor.calledComponents()).containsExactlyInAnyOrder(file); } @Test diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java index 267c06f989f..32bfa672ff4 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java @@ -30,7 +30,7 @@ public class IssueWsModuleTest { public void verify_count_of_added_components() { ComponentContainer container = new ComponentContainer(); new IssueWsModule().configure(container); - assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 33); + assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 31); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetSeverityActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetSeverityActionTest.java index 30d2a740ba7..545ce4d5ec7 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetSeverityActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetSeverityActionTest.java @@ -45,6 +45,7 @@ import org.sonar.server.issue.IssueFieldsSetter; import org.sonar.server.issue.IssueFinder; import org.sonar.server.issue.IssueUpdater; import org.sonar.server.issue.ServerIssueStorage; +import org.sonar.server.issue.TestIssueChangePostProcessor; import org.sonar.server.issue.index.IssueIndexDefinition; import org.sonar.server.issue.index.IssueIndexer; import org.sonar.server.issue.index.IssueIteratorFactory; @@ -91,13 +92,14 @@ public class SetSeverityActionTest { private ArgumentCaptor preloadedSearchResponseDataCaptor = ArgumentCaptor.forClass(SearchResponseData.class); private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), dbClient, new IssueIteratorFactory(dbClient)); + private TestIssueChangePostProcessor issueChangePostProcessor = new TestIssueChangePostProcessor(); private WsActionTester tester = new WsActionTester(new SetSeverityAction(userSession, dbClient, new IssueFinder(dbClient, userSession), new IssueFieldsSetter(), new IssueUpdater(dbClient, - new ServerIssueStorage(system2, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), dbClient, issueIndexer), mock(NotificationManager.class)), + new ServerIssueStorage(system2, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), dbClient, issueIndexer), mock(NotificationManager.class), issueChangePostProcessor), responseWriter)); @Test - public void set_severity() throws Exception { + public void set_severity() { IssueDto issueDto = issueDbTester.insertIssue(newIssue().setSeverity(MAJOR)); setUserWithBrowseAndAdministerIssuePermission(issueDto); @@ -109,10 +111,13 @@ public class SetSeverityActionTest { IssueDto issueReloaded = dbClient.issueDao().selectByKey(dbTester.getSession(), issueDto.getKey()).get(); assertThat(issueReloaded.getSeverity()).isEqualTo(MINOR); assertThat(issueReloaded.isManualSeverity()).isTrue(); + assertThat(issueChangePostProcessor.calledComponents()) + .extracting(ComponentDto::uuid) + .containsExactlyInAnyOrder(issueDto.getComponentUuid()); } @Test - public void insert_entry_in_changelog_when_setting_severity() throws Exception { + public void insert_entry_in_changelog_when_setting_severity() { IssueDto issueDto = issueDbTester.insertIssue(newIssue().setSeverity(MAJOR)); setUserWithBrowseAndAdministerIssuePermission(issueDto); @@ -136,13 +141,13 @@ public class SetSeverityActionTest { } @Test - public void fail_when_not_authenticated() throws Exception { + public void fail_when_not_authenticated() { expectedException.expect(UnauthorizedException.class); call("ABCD", MAJOR); } @Test - public void fail_when_missing_browse_permission() throws Exception { + public void fail_when_missing_browse_permission() { IssueDto issueDto = issueDbTester.insertIssue(); logInAndAddProjectPermission(issueDto, ISSUE_ADMIN); @@ -151,7 +156,7 @@ public class SetSeverityActionTest { } @Test - public void fail_when_missing_administer_issue_permission() throws Exception { + public void fail_when_missing_administer_issue_permission() { IssueDto issueDto = issueDbTester.insertIssue(); logInAndAddProjectPermission(issueDto, USER); diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTagsActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTagsActionTest.java index 5df04b4ee93..22e9634f98d 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTagsActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTagsActionTest.java @@ -47,6 +47,7 @@ import org.sonar.server.issue.IssueFieldsSetter; import org.sonar.server.issue.IssueFinder; import org.sonar.server.issue.IssueUpdater; import org.sonar.server.issue.ServerIssueStorage; +import org.sonar.server.issue.TestIssueChangePostProcessor; import org.sonar.server.issue.index.IssueIndexDefinition; import org.sonar.server.issue.index.IssueIndexer; import org.sonar.server.issue.index.IssueIteratorFactory; @@ -87,10 +88,11 @@ public class SetTagsActionTest { private OperationResponseWriter responseWriter = mock(OperationResponseWriter.class); private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), dbClient, new IssueIteratorFactory(dbClient)); private ArgumentCaptor preloadedSearchResponseDataCaptor = ArgumentCaptor.forClass(SearchResponseData.class); + private TestIssueChangePostProcessor issueChangePostProcessor = new TestIssueChangePostProcessor(); private WsActionTester ws = new WsActionTester(new SetTagsAction(userSession, dbClient, new IssueFinder(dbClient, userSession), new IssueFieldsSetter(), new IssueUpdater(dbClient, - new ServerIssueStorage(system2, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), dbClient, issueIndexer), mock(NotificationManager.class)), + new ServerIssueStorage(system2, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), dbClient, issueIndexer), mock(NotificationManager.class), issueChangePostProcessor), responseWriter)); @Test @@ -104,6 +106,7 @@ public class SetTagsActionTest { verifyContentOfPreloadedSearchResponseData(issueDto); IssueDto issueReloaded = dbClient.issueDao().selectByKey(db.getSession(), issueDto.getKey()).get(); assertThat(issueReloaded.getTags()).containsOnly("bug", "todo"); + assertThat(issueChangePostProcessor.wasCalled()).isFalse(); } @Test @@ -115,6 +118,7 @@ public class SetTagsActionTest { IssueDto issueReloaded = dbClient.issueDao().selectByKey(db.getSession(), issueDto.getKey()).get(); assertThat(issueReloaded.getTags()).isEmpty(); + assertThat(issueChangePostProcessor.wasCalled()).isFalse(); } @Test diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java index c7edce5e1f5..e51d37abab2 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java @@ -19,8 +19,6 @@ */ package org.sonar.server.issue.ws; -import java.util.Collections; -import java.util.Date; import java.util.List; import javax.annotation.Nullable; import org.junit.Rule; @@ -32,9 +30,7 @@ import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; import org.sonar.api.utils.System2; -import org.sonar.core.issue.DefaultIssue; import org.sonar.core.issue.FieldDiffs; -import org.sonar.core.issue.IssueChangeContext; import org.sonar.db.DbClient; import org.sonar.db.DbTester; import org.sonar.db.component.ComponentDto; @@ -49,15 +45,13 @@ import org.sonar.server.issue.IssueFieldsSetter; import org.sonar.server.issue.IssueFinder; import org.sonar.server.issue.IssueUpdater; import org.sonar.server.issue.ServerIssueStorage; +import org.sonar.server.issue.TestIssueChangePostProcessor; import org.sonar.server.issue.index.IssueIndexDefinition; import org.sonar.server.issue.index.IssueIndexer; import org.sonar.server.issue.index.IssueIteratorFactory; import org.sonar.server.notification.NotificationManager; import org.sonar.server.organization.DefaultOrganizationProvider; import org.sonar.server.organization.TestDefaultOrganizationProvider; -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.rule.DefaultRuleFinder; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.TestRequest; @@ -100,12 +94,12 @@ public class SetTypeActionTest { private ArgumentCaptor preloadedSearchResponseDataCaptor = ArgumentCaptor.forClass(SearchResponseData.class); private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), dbClient, new IssueIteratorFactory(dbClient)); - private QGChangeEventFactory qgChangeEventFactory = mock(QGChangeEventFactory.class); - private QGChangeEventListeners qgChangeEventListeners = mock(QGChangeEventListeners.class); + private TestIssueChangePostProcessor issueChangePostProcessor = new TestIssueChangePostProcessor(); private WsActionTester tester = new WsActionTester(new SetTypeAction(userSession, dbClient, new IssueFinder(dbClient, userSession), new IssueFieldsSetter(), new IssueUpdater(dbClient, - new ServerIssueStorage(system2, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), dbClient, issueIndexer), mock(NotificationManager.class)), - responseWriter, system2, qgChangeEventFactory, qgChangeEventListeners)); + new ServerIssueStorage(system2, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), dbClient, issueIndexer), mock(NotificationManager.class), + issueChangePostProcessor), + responseWriter, system2)); @Test public void set_type() { @@ -113,11 +107,6 @@ public class SetTypeActionTest { when(system2.now()).thenReturn(now); IssueDto issueDto = issueDbTester.insertIssue(newIssue().setType(CODE_SMELL)); setUserWithBrowseAndAdministerIssuePermission(issueDto); - QGChangeEventFactory.IssueChange issueChange = new QGChangeEventFactory.IssueChange(BUG, null); - IssueChangeContext changeContext = IssueChangeContext.createUser(new Date(now), userSession.getLogin()); - List qgChangeEvents = Collections.singletonList(mock(QGChangeEvent.class)); - when(qgChangeEventFactory.from(any(QGChangeEventFactory.IssueChangeData.class), eq(issueChange), eq(changeContext))) - .thenReturn(qgChangeEvents); call(issueDto.getKey(), BUG.name()); @@ -126,16 +115,9 @@ public class SetTypeActionTest { IssueDto issueReloaded = dbClient.issueDao().selectByKey(dbTester.getSession(), issueDto.getKey()).get(); assertThat(issueReloaded.getType()).isEqualTo(BUG.getDbConstant()); - ArgumentCaptor issueChangeDataCaptor = ArgumentCaptor.forClass(QGChangeEventFactory.IssueChangeData.class); - verify(qgChangeEventFactory).from(issueChangeDataCaptor.capture(), eq(issueChange), eq(changeContext)); - QGChangeEventFactory.IssueChangeData issueChangeData = issueChangeDataCaptor.getValue(); - assertThat(issueChangeData.getIssues()) - .extracting(DefaultIssue::key) - .containsOnly(issueDto.getKey()); - assertThat(issueChangeData.getComponents()) + assertThat(issueChangePostProcessor.calledComponents()) .extracting(ComponentDto::uuid) - .containsOnly(issueDto.getComponentUuid(), issueDto.getProjectUuid()); - verify(qgChangeEventListeners).broadcastOnIssueChange(issueChangeData, qgChangeEvents); + .containsExactlyInAnyOrder(issueDto.getComponentUuid()); } @Test diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImplTest.java new file mode 100644 index 00000000000..8e7d34e9f53 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImplTest.java @@ -0,0 +1,832 @@ +/* + * 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.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.issue.Issue; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.measures.Metric; +import org.sonar.api.rule.Severity; +import org.sonar.api.rules.RuleType; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.issue.IssueGroupDto; +import org.sonar.server.computation.task.projectanalysis.qualitymodel.DebtRatingGrid; +import org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; + +public class IssueMetricFormulaFactoryImplTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private IssueMetricFormulaFactoryImpl underTest = new IssueMetricFormulaFactoryImpl(); + + @Test + public void getFormulaMetrics_include_the_dependent_metrics() { + for (IssueMetricFormula formula : underTest.getFormulas()) { + assertThat(underTest.getFormulaMetrics()).contains(formula.getMetric()); + for (Metric dependentMetric : formula.getDependentMetrics()) { + assertThat(underTest.getFormulaMetrics()).contains(dependentMetric); + } + } + } + + @Test + public void test_violations() { + withNoIssues().assertThatValueIs(CoreMetrics.VIOLATIONS, 0); + with(newGroup(), newGroup().setCount(4)).assertThatValueIs(CoreMetrics.VIOLATIONS, 5); + + // exclude resolved + IssueGroupDto resolved = newResolvedGroup(Issue.RESOLUTION_FIXED, Issue.STATUS_RESOLVED); + with(newGroup(), newGroup(), resolved).assertThatValueIs(CoreMetrics.VIOLATIONS, 2); + + // include issues on leak + IssueGroupDto onLeak = newGroup().setCount(11).setInLeak(true); + with(newGroup(), newGroup(), onLeak).assertThatValueIs(CoreMetrics.VIOLATIONS, 1 + 1 + 11); + } + + @Test + public void test_bugs() { + withNoIssues().assertThatValueIs(CoreMetrics.BUGS, 0); + with( + newGroup(RuleType.BUG).setSeverity(Severity.MAJOR).setCount(3), + newGroup(RuleType.BUG).setSeverity(Severity.CRITICAL).setCount(5), + // exclude resolved + newResolvedGroup(RuleType.BUG).setCount(7), + // not bugs + newGroup(RuleType.CODE_SMELL).setCount(11)) + .assertThatValueIs(CoreMetrics.BUGS, 3 + 5); + } + + @Test + public void test_code_smells() { + withNoIssues().assertThatValueIs(CoreMetrics.CODE_SMELLS, 0); + with( + newGroup(RuleType.CODE_SMELL).setSeverity(Severity.MAJOR).setCount(3), + newGroup(RuleType.CODE_SMELL).setSeverity(Severity.CRITICAL).setCount(5), + // exclude resolved + newResolvedGroup(RuleType.CODE_SMELL).setCount(7), + // not code smells + newGroup(RuleType.BUG).setCount(11)) + .assertThatValueIs(CoreMetrics.CODE_SMELLS, 3 + 5); + } + + @Test + public void test_vulnerabilities() { + withNoIssues().assertThatValueIs(CoreMetrics.VULNERABILITIES, 0); + with( + newGroup(RuleType.VULNERABILITY).setSeverity(Severity.MAJOR).setCount(3), + newGroup(RuleType.VULNERABILITY).setSeverity(Severity.CRITICAL).setCount(5), + // exclude resolved + newResolvedGroup(RuleType.VULNERABILITY).setCount(7), + // not vulnerabilities + newGroup(RuleType.BUG).setCount(11)) + .assertThatValueIs(CoreMetrics.VULNERABILITIES, 3 + 5); + } + + @Test + public void count_unresolved_by_severity() { + withNoIssues() + .assertThatValueIs(CoreMetrics.BLOCKER_VIOLATIONS, 0) + .assertThatValueIs(CoreMetrics.CRITICAL_VIOLATIONS, 0) + .assertThatValueIs(CoreMetrics.MAJOR_VIOLATIONS, 0) + .assertThatValueIs(CoreMetrics.MINOR_VIOLATIONS, 0) + .assertThatValueIs(CoreMetrics.INFO_VIOLATIONS, 0); + + with( + newGroup(RuleType.VULNERABILITY).setSeverity(Severity.MAJOR).setCount(3), + newGroup(RuleType.BUG).setSeverity(Severity.MAJOR).setCount(5), + newGroup(RuleType.BUG).setSeverity(Severity.CRITICAL).setCount(7), + newGroup(RuleType.CODE_SMELL).setSeverity(Severity.BLOCKER).setCount(11), + // include leak + newGroup(RuleType.CODE_SMELL).setSeverity(Severity.BLOCKER).setInLeak(true).setCount(13), + // exclude resolved + newResolvedGroup(RuleType.VULNERABILITY).setSeverity(Severity.INFO).setCount(17), + newResolvedGroup(RuleType.BUG).setSeverity(Severity.MAJOR).setCount(19)) + .assertThatValueIs(CoreMetrics.BLOCKER_VIOLATIONS, 11 + 13) + .assertThatValueIs(CoreMetrics.CRITICAL_VIOLATIONS, 7) + .assertThatValueIs(CoreMetrics.MAJOR_VIOLATIONS, 3 + 5) + .assertThatValueIs(CoreMetrics.MINOR_VIOLATIONS, 0) + .assertThatValueIs(CoreMetrics.INFO_VIOLATIONS, 0); + } + + @Test + public void count_resolved() { + withNoIssues() + .assertThatValueIs(CoreMetrics.FALSE_POSITIVE_ISSUES, 0) + .assertThatValueIs(CoreMetrics.WONT_FIX_ISSUES, 0); + + with( + newResolvedGroup(Issue.RESOLUTION_FIXED, Issue.STATUS_RESOLVED).setCount(3), + newResolvedGroup(Issue.RESOLUTION_FALSE_POSITIVE, Issue.STATUS_CLOSED).setCount(5), + newResolvedGroup(Issue.RESOLUTION_WONT_FIX, Issue.STATUS_CLOSED).setSeverity(Severity.MAJOR).setCount(7), + newResolvedGroup(Issue.RESOLUTION_WONT_FIX, Issue.STATUS_CLOSED).setSeverity(Severity.BLOCKER).setCount(11), + newResolvedGroup(Issue.RESOLUTION_REMOVED, Issue.STATUS_CLOSED).setCount(13), + // exclude unresolved + newGroup(RuleType.VULNERABILITY).setCount(17), + newGroup(RuleType.BUG).setCount(19)) + .assertThatValueIs(CoreMetrics.FALSE_POSITIVE_ISSUES, 5) + .assertThatValueIs(CoreMetrics.WONT_FIX_ISSUES, 7 + 11); + } + + @Test + public void count_by_status() { + withNoIssues() + .assertThatValueIs(CoreMetrics.CONFIRMED_ISSUES, 0) + .assertThatValueIs(CoreMetrics.OPEN_ISSUES, 0) + .assertThatValueIs(CoreMetrics.REOPENED_ISSUES, 0); + + with( + newGroup().setStatus(Issue.STATUS_CONFIRMED).setSeverity(Severity.BLOCKER).setCount(3), + newGroup().setStatus(Issue.STATUS_CONFIRMED).setSeverity(Severity.INFO).setCount(5), + newGroup().setStatus(Issue.STATUS_REOPENED).setCount(7), + newGroup(RuleType.CODE_SMELL).setStatus(Issue.STATUS_OPEN).setCount(9), + newGroup(RuleType.BUG).setStatus(Issue.STATUS_OPEN).setCount(11), + newResolvedGroup(Issue.RESOLUTION_FALSE_POSITIVE, Issue.STATUS_CLOSED).setCount(13)) + .assertThatValueIs(CoreMetrics.CONFIRMED_ISSUES, 3 + 5) + .assertThatValueIs(CoreMetrics.OPEN_ISSUES, 9 + 11) + .assertThatValueIs(CoreMetrics.REOPENED_ISSUES, 7); + } + + @Test + public void test_technical_debt() { + withNoIssues().assertThatValueIs(CoreMetrics.TECHNICAL_DEBT, 0); + + with( + newGroup(RuleType.CODE_SMELL).setEffort(3.0).setInLeak(false), + newGroup(RuleType.CODE_SMELL).setEffort(5.0).setInLeak(true), + // not code smells + newGroup(RuleType.BUG).setEffort(7.0), + // exclude resolved + newResolvedGroup(RuleType.CODE_SMELL).setEffort(17.0)) + .assertThatValueIs(CoreMetrics.TECHNICAL_DEBT, 3.0 + 5.0); + } + + @Test + public void test_reliability_remediation_effort() { + withNoIssues().assertThatValueIs(CoreMetrics.RELIABILITY_REMEDIATION_EFFORT, 0); + + with( + newGroup(RuleType.BUG).setEffort(3.0), + newGroup(RuleType.BUG).setEffort(5.0).setSeverity(Severity.BLOCKER), + // not bugs + newGroup(RuleType.CODE_SMELL).setEffort(7.0), + // exclude resolved + newResolvedGroup(RuleType.BUG).setEffort(17.0)) + .assertThatValueIs(CoreMetrics.RELIABILITY_REMEDIATION_EFFORT, 3.0 + 5.0); + } + + @Test + public void test_security_remediation_effort() { + withNoIssues().assertThatValueIs(CoreMetrics.SECURITY_REMEDIATION_EFFORT, 0); + + with( + newGroup(RuleType.VULNERABILITY).setEffort(3.0), + newGroup(RuleType.VULNERABILITY).setEffort(5.0).setSeverity(Severity.BLOCKER), + // not vulnerability + newGroup(RuleType.CODE_SMELL).setEffort(7.0), + // exclude resolved + newResolvedGroup(RuleType.VULNERABILITY).setEffort(17.0)) + .assertThatValueIs(CoreMetrics.SECURITY_REMEDIATION_EFFORT, 3.0 + 5.0); + } + + @Test + public void test_sqale_debt_ratio_and_sqale_rating() { + withNoIssues() + .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0) + .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A); + + // technical_debt not computed + with(CoreMetrics.DEVELOPMENT_COST, 0) + .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0) + .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A); + with(CoreMetrics.DEVELOPMENT_COST, 20) + .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0) + .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A); + + // development_cost not computed + with(CoreMetrics.TECHNICAL_DEBT, 0) + .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0) + .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A); + with(CoreMetrics.TECHNICAL_DEBT, 20) + .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0) + .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A); + + // input measures are available + with(CoreMetrics.TECHNICAL_DEBT, 20.0) + .and(CoreMetrics.DEVELOPMENT_COST, 0.0) + .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0.0) + .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A); + + with(CoreMetrics.TECHNICAL_DEBT, 20.0) + .and(CoreMetrics.DEVELOPMENT_COST, 160.0) + .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 12.5) + .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.C); + + with(CoreMetrics.TECHNICAL_DEBT, 20.0) + .and(CoreMetrics.DEVELOPMENT_COST, 10.0) + .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 200.0) + .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.E); + + // B is 5% --> min debt is exactly 200*0.05=10 + with(CoreMetrics.DEVELOPMENT_COST, 200.0) + .and(CoreMetrics.TECHNICAL_DEBT, 10.0) + .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 5.0) + .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.B); + + with(CoreMetrics.TECHNICAL_DEBT, 0.0) + .and(CoreMetrics.DEVELOPMENT_COST, 0.0) + .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0.0) + .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A); + + with(CoreMetrics.TECHNICAL_DEBT, 0.0) + .and(CoreMetrics.DEVELOPMENT_COST, 80.0) + .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0.0); + + with(CoreMetrics.TECHNICAL_DEBT, -20.0) + .and(CoreMetrics.DEVELOPMENT_COST, 0.0) + .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0.0) + .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A); + + // bug, debt can't be negative + with(CoreMetrics.TECHNICAL_DEBT, -20.0) + .and(CoreMetrics.DEVELOPMENT_COST, 80.0) + .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0.0) + .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A); + + // bug, cost can't be negative + with(CoreMetrics.TECHNICAL_DEBT, 20.0) + .and(CoreMetrics.DEVELOPMENT_COST, -80.0) + .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0.0) + .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A); + } + + @Test + public void test_effort_to_reach_maintainability_rating_A() { + withNoIssues() + .assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 0.0); + + // technical_debt not computed + with(CoreMetrics.DEVELOPMENT_COST, 0.0) + .assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 0.0); + with(CoreMetrics.DEVELOPMENT_COST, 20.0) + .assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 0.0); + + // development_cost not computed + with(CoreMetrics.TECHNICAL_DEBT, 0.0) + .assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 0.0); + with(CoreMetrics.TECHNICAL_DEBT, 20.0) + // development cost is considered as zero, so the effort is to reach... zero + .assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 20.0); + + // B to A + with(CoreMetrics.DEVELOPMENT_COST, 200.0) + .and(CoreMetrics.TECHNICAL_DEBT, 40.0) + // B is 5% --> goal is to reach 200*0.05=10 --> effort is 40-10=30 + .assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 40.0 - (200.0 * 0.05)); + + // E to A + with(CoreMetrics.DEVELOPMENT_COST, 200.0) + .and(CoreMetrics.TECHNICAL_DEBT, 180.0) + // B is 5% --> goal is to reach 200*0.05=10 --> effort is 180-10=170 + .assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 180.0 - (200.0 * 0.05)); + + // already A + with(CoreMetrics.DEVELOPMENT_COST, 200.0) + .and(CoreMetrics.TECHNICAL_DEBT, 8.0) + // B is 5% --> goal is to reach 200*0.05=10 --> debt is already at 8 --> effort to reach A is zero + .assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 0.0); + + // exactly lower range of B + with(CoreMetrics.DEVELOPMENT_COST, 200.0) + .and(CoreMetrics.TECHNICAL_DEBT, 10.0) + // B is 5% --> goal is to reach 200*0.05=10 --> debt is 10 --> effort to reach A is zero + // FIXME need zero to reach A but effective rating is B ! + .assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 0.0); + } + + @Test + public void test_reliability_rating() { + withNoIssues() + .assertThatValueIs(CoreMetrics.RELIABILITY_RATING, Rating.A); + + with( + newGroup(RuleType.BUG).setSeverity(Severity.CRITICAL).setCount(1), + newGroup(RuleType.BUG).setSeverity(Severity.MINOR).setCount(5), + // excluded, not a bug + newGroup(RuleType.CODE_SMELL).setSeverity(Severity.BLOCKER).setCount(3)) + // highest severity of bugs is CRITICAL --> D + .assertThatValueIs(CoreMetrics.RELIABILITY_RATING, Rating.D); + + with( + newGroup(RuleType.CODE_SMELL).setSeverity(Severity.MAJOR).setCount(3), + newGroup(RuleType.VULNERABILITY).setSeverity(Severity.CRITICAL).setCount(5)) + // no bugs --> A + .assertThatValueIs(CoreMetrics.RELIABILITY_RATING, Rating.A); + } + + @Test + public void test_security_rating() { + withNoIssues() + .assertThatValueIs(CoreMetrics.SECURITY_RATING, Rating.A); + + with( + newGroup(RuleType.VULNERABILITY).setSeverity(Severity.CRITICAL).setCount(1), + newGroup(RuleType.VULNERABILITY).setSeverity(Severity.MINOR).setCount(5), + // excluded, not a vulnerability + newGroup(RuleType.CODE_SMELL).setSeverity(Severity.BLOCKER).setCount(3)) + // highest severity of vulnerabilities is CRITICAL --> D + .assertThatValueIs(CoreMetrics.SECURITY_RATING, Rating.D); + + with( + newGroup(RuleType.CODE_SMELL).setSeverity(Severity.MAJOR).setCount(3), + newGroup(RuleType.BUG).setSeverity(Severity.CRITICAL).setCount(5)) + // no vulnerabilities --> A + .assertThatValueIs(CoreMetrics.SECURITY_RATING, Rating.A); + } + + @Test + public void test_new_bugs() { + withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_BUGS, 0.0); + + with( + newGroup(RuleType.BUG).setInLeak(false).setSeverity(Severity.MAJOR).setCount(3), + newGroup(RuleType.BUG).setInLeak(true).setSeverity(Severity.CRITICAL).setCount(5), + newGroup(RuleType.BUG).setInLeak(true).setSeverity(Severity.MINOR).setCount(7), + // not bugs + newGroup(RuleType.CODE_SMELL).setInLeak(true).setCount(9), + newGroup(RuleType.VULNERABILITY).setInLeak(true).setCount(11)) + .assertThatLeakValueIs(CoreMetrics.NEW_BUGS, 5 + 7); + } + + @Test + public void test_new_code_smells() { + withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_CODE_SMELLS, 0.0); + + with( + newGroup(RuleType.CODE_SMELL).setInLeak(false).setSeverity(Severity.MAJOR).setCount(3), + newGroup(RuleType.CODE_SMELL).setInLeak(true).setSeverity(Severity.CRITICAL).setCount(5), + newGroup(RuleType.CODE_SMELL).setInLeak(true).setSeverity(Severity.MINOR).setCount(7), + // not code smells + newGroup(RuleType.BUG).setInLeak(true).setCount(9), + newGroup(RuleType.VULNERABILITY).setInLeak(true).setCount(11)) + .assertThatLeakValueIs(CoreMetrics.NEW_CODE_SMELLS, 5 + 7); + } + + @Test + public void test_new_vulnerabilities() { + withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_VULNERABILITIES, 0.0); + + with( + newGroup(RuleType.VULNERABILITY).setInLeak(false).setSeverity(Severity.MAJOR).setCount(3), + newGroup(RuleType.VULNERABILITY).setInLeak(true).setSeverity(Severity.CRITICAL).setCount(5), + newGroup(RuleType.VULNERABILITY).setInLeak(true).setSeverity(Severity.MINOR).setCount(7), + // not vulnerabilities + newGroup(RuleType.BUG).setInLeak(true).setCount(9), + newGroup(RuleType.CODE_SMELL).setInLeak(true).setCount(11)) + .assertThatLeakValueIs(CoreMetrics.NEW_VULNERABILITIES, 5 + 7); + } + + @Test + public void test_new_violations() { + withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_VIOLATIONS, 0.0); + + with( + newGroup(RuleType.BUG).setInLeak(true).setCount(5), + newGroup(RuleType.CODE_SMELL).setInLeak(true).setCount(7), + newGroup(RuleType.VULNERABILITY).setInLeak(true).setCount(9), + // not in leak + newGroup(RuleType.BUG).setInLeak(false).setCount(11), + newGroup(RuleType.CODE_SMELL).setInLeak(false).setCount(13), + newGroup(RuleType.VULNERABILITY).setInLeak(false).setCount(17)) + .assertThatLeakValueIs(CoreMetrics.NEW_VIOLATIONS, 5 + 7 + 9); + } + + @Test + public void test_new_blocker_violations() { + withNoIssues() + .assertThatLeakValueIs(CoreMetrics.NEW_BLOCKER_VIOLATIONS, 0.0); + + with( + newGroup(RuleType.CODE_SMELL).setSeverity(Severity.BLOCKER).setInLeak(true).setCount(3), + newGroup(RuleType.BUG).setSeverity(Severity.BLOCKER).setInLeak(true).setCount(5), + newGroup(RuleType.VULNERABILITY).setSeverity(Severity.BLOCKER).setInLeak(true).setCount(7), + // not blocker + newGroup(RuleType.CODE_SMELL).setSeverity(Severity.CRITICAL).setInLeak(true).setCount(9), + // not in leak + newGroup(RuleType.CODE_SMELL).setSeverity(Severity.BLOCKER).setInLeak(false).setCount(11), + newGroup(RuleType.BUG).setSeverity(Severity.BLOCKER).setInLeak(false).setCount(13)) + .assertThatLeakValueIs(CoreMetrics.NEW_BLOCKER_VIOLATIONS, 3 + 5 + 7); + } + + @Test + public void test_new_critical_violations() { + withNoIssues() + .assertThatLeakValueIs(CoreMetrics.NEW_CRITICAL_VIOLATIONS, 0.0); + + with( + newGroup(RuleType.CODE_SMELL).setSeverity(Severity.CRITICAL).setInLeak(true).setCount(3), + newGroup(RuleType.BUG).setSeverity(Severity.CRITICAL).setInLeak(true).setCount(5), + newGroup(RuleType.VULNERABILITY).setSeverity(Severity.CRITICAL).setInLeak(true).setCount(7), + // not CRITICAL + newGroup(RuleType.CODE_SMELL).setSeverity(Severity.MAJOR).setInLeak(true).setCount(9), + // not in leak + newGroup(RuleType.CODE_SMELL).setSeverity(Severity.CRITICAL).setInLeak(false).setCount(11), + newGroup(RuleType.BUG).setSeverity(Severity.CRITICAL).setInLeak(false).setCount(13)) + .assertThatLeakValueIs(CoreMetrics.NEW_CRITICAL_VIOLATIONS, 3 + 5 + 7); + } + + @Test + public void test_new_major_violations() { + withNoIssues() + .assertThatLeakValueIs(CoreMetrics.NEW_MAJOR_VIOLATIONS, 0.0); + + with( + newGroup(RuleType.CODE_SMELL).setSeverity(Severity.MAJOR).setInLeak(true).setCount(3), + newGroup(RuleType.BUG).setSeverity(Severity.MAJOR).setInLeak(true).setCount(5), + newGroup(RuleType.VULNERABILITY).setSeverity(Severity.MAJOR).setInLeak(true).setCount(7), + // not MAJOR + newGroup(RuleType.CODE_SMELL).setSeverity(Severity.CRITICAL).setInLeak(true).setCount(9), + // not in leak + newGroup(RuleType.CODE_SMELL).setSeverity(Severity.MAJOR).setInLeak(false).setCount(11), + newGroup(RuleType.BUG).setSeverity(Severity.MAJOR).setInLeak(false).setCount(13)) + .assertThatLeakValueIs(CoreMetrics.NEW_MAJOR_VIOLATIONS, 3 + 5 + 7); + } + + @Test + public void test_new_minor_violations() { + withNoIssues() + .assertThatLeakValueIs(CoreMetrics.NEW_MINOR_VIOLATIONS, 0.0); + + with( + newGroup(RuleType.CODE_SMELL).setSeverity(Severity.MINOR).setInLeak(true).setCount(3), + newGroup(RuleType.BUG).setSeverity(Severity.MINOR).setInLeak(true).setCount(5), + newGroup(RuleType.VULNERABILITY).setSeverity(Severity.MINOR).setInLeak(true).setCount(7), + // not MINOR + newGroup(RuleType.CODE_SMELL).setSeverity(Severity.CRITICAL).setInLeak(true).setCount(9), + // not in leak + newGroup(RuleType.CODE_SMELL).setSeverity(Severity.MINOR).setInLeak(false).setCount(11), + newGroup(RuleType.BUG).setSeverity(Severity.MINOR).setInLeak(false).setCount(13)) + .assertThatLeakValueIs(CoreMetrics.NEW_MINOR_VIOLATIONS, 3 + 5 + 7); + } + + @Test + public void test_new_info_violations() { + withNoIssues() + .assertThatLeakValueIs(CoreMetrics.NEW_INFO_VIOLATIONS, 0.0); + + with( + newGroup(RuleType.CODE_SMELL).setSeverity(Severity.INFO).setInLeak(true).setCount(3), + newGroup(RuleType.BUG).setSeverity(Severity.INFO).setInLeak(true).setCount(5), + newGroup(RuleType.VULNERABILITY).setSeverity(Severity.INFO).setInLeak(true).setCount(7), + // not INFO + newGroup(RuleType.CODE_SMELL).setSeverity(Severity.CRITICAL).setInLeak(true).setCount(9), + // not in leak + newGroup(RuleType.CODE_SMELL).setSeverity(Severity.INFO).setInLeak(false).setCount(11), + newGroup(RuleType.BUG).setSeverity(Severity.INFO).setInLeak(false).setCount(13)) + .assertThatLeakValueIs(CoreMetrics.NEW_INFO_VIOLATIONS, 3 + 5 + 7); + } + + @Test + public void test_new_technical_debt() { + withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_TECHNICAL_DEBT, 0.0); + + with( + newGroup(RuleType.CODE_SMELL).setEffort(3.0).setInLeak(true), + // not in leak + newGroup(RuleType.CODE_SMELL).setEffort(5.0).setInLeak(false), + // not code smells + newGroup(RuleType.BUG).setEffort(7.0).setInLeak(true), + // exclude resolved + newResolvedGroup(RuleType.CODE_SMELL).setEffort(17.0).setInLeak(true)) + .assertThatLeakValueIs(CoreMetrics.NEW_TECHNICAL_DEBT, 3.0); + } + + @Test + public void test_new_reliability_remediation_effort() { + withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_RELIABILITY_REMEDIATION_EFFORT, 0.0); + + with( + newGroup(RuleType.BUG).setEffort(3.0).setInLeak(true), + // not in leak + newGroup(RuleType.BUG).setEffort(5.0).setInLeak(false), + // not bugs + newGroup(RuleType.CODE_SMELL).setEffort(7.0).setInLeak(true), + // exclude resolved + newResolvedGroup(RuleType.BUG).setEffort(17.0).setInLeak(true)) + .assertThatLeakValueIs(CoreMetrics.NEW_RELIABILITY_REMEDIATION_EFFORT, 3.0); + } + + @Test + public void test_new_security_remediation_effort() { + withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_REMEDIATION_EFFORT, 0.0); + + with( + newGroup(RuleType.VULNERABILITY).setEffort(3.0).setInLeak(true), + // not in leak + newGroup(RuleType.VULNERABILITY).setEffort(5.0).setInLeak(false), + // not vulnerability + newGroup(RuleType.CODE_SMELL).setEffort(7.0).setInLeak(true), + // exclude resolved + newResolvedGroup(RuleType.VULNERABILITY).setEffort(17.0).setInLeak(true)) + .assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_REMEDIATION_EFFORT, 3.0); + } + + @Test + public void test_new_reliability_rating() { + withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_RELIABILITY_RATING, Rating.A); + + with( + newGroup(RuleType.BUG).setSeverity(Severity.INFO).setCount(3).setInLeak(true), + newGroup(RuleType.BUG).setSeverity(Severity.MINOR).setCount(1).setInLeak(true), + // not in leak + newGroup(RuleType.BUG).setSeverity(Severity.BLOCKER).setInLeak(false), + // not bug + newGroup(RuleType.CODE_SMELL).setSeverity(Severity.BLOCKER).setInLeak(true), + // exclude resolved + newResolvedGroup(RuleType.BUG).setSeverity(Severity.BLOCKER).setInLeak(true)) + // highest severity of bugs on leak period is minor -> B + .assertThatLeakValueIs(CoreMetrics.NEW_RELIABILITY_RATING, Rating.B); + } + + @Test + public void test_new_security_rating() { + withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_RATING, Rating.A); + + with( + newGroup(RuleType.VULNERABILITY).setSeverity(Severity.INFO).setCount(3).setInLeak(true), + newGroup(RuleType.VULNERABILITY).setSeverity(Severity.MINOR).setCount(1).setInLeak(true), + // not in leak + newGroup(RuleType.VULNERABILITY).setSeverity(Severity.BLOCKER).setInLeak(false), + // not vulnerability + newGroup(RuleType.CODE_SMELL).setSeverity(Severity.BLOCKER).setInLeak(true), + // exclude resolved + newResolvedGroup(RuleType.VULNERABILITY).setSeverity(Severity.BLOCKER).setInLeak(true)) + // highest severity of bugs on leak period is minor -> B + .assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_RATING, Rating.B); + } + + @Test + public void test_new_sqale_debt_ratio_and_new_maintainability_rating() { + withNoIssues() + .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0) + .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A); + + // technical_debt not computed + withLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 0) + .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0) + .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A); + withLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 20) + .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0) + .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A); + + // development_cost not computed + withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, 0) + .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0) + .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A); + withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, 20) + .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0) + .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A); + + // input measures are available + withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, 20.0) + .andLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 0.0) + .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0.0) + .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A); + + withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, 20.0) + .andLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 160.0) + .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 12.5) + .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.C); + + withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, 20.0) + .andLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 10.0) + .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 200.0) + .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.E); + + // B is 5% --> min debt is exactly 200*0.05=10 + withLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 200.0) + .andLeak(CoreMetrics.NEW_TECHNICAL_DEBT, 10.0) + .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 5.0) + .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.B); + + withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, 0.0) + .andLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 0.0) + .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0.0) + .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A); + + withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, 0.0) + .andLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 80.0) + .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0.0); + + withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, -20.0) + .andLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 0.0) + .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0.0) + .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A); + + // bug, debt can't be negative + withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, -20.0) + .andLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 80.0) + .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0.0) + .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A); + + // bug, cost can't be negative + withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, 20.0) + .andLeak(CoreMetrics.NEW_DEVELOPMENT_COST, -80.0) + .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0.0) + .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A); + } + + private Verifier with(IssueGroupDto... groups) { + return new Verifier(groups); + } + + private Verifier withNoIssues() { + return new Verifier(new IssueGroupDto[0]); + } + + private Verifier with(Metric metric, double value) { + return new Verifier(new IssueGroupDto[0]).and(metric, value); + } + + private Verifier withLeak(Metric metric, double leakValue) { + return new Verifier(new IssueGroupDto[0]).andLeak(metric, leakValue); + } + + private class Verifier { + private final IssueGroupDto[] groups; + private final Map values = new HashMap<>(); + private final Map leakValues = new HashMap<>(); + + private Verifier(IssueGroupDto[] groups) { + this.groups = groups; + } + + Verifier and(Metric metric, double value) { + this.values.put(metric, value); + return this; + } + + Verifier andLeak(Metric metric, double value) { + this.leakValues.put(metric, value); + return this; + } + + Verifier assertThatValueIs(Metric metric, double expectedValue) { + TestContext context = run(metric, false); + assertThat(context.doubleValue).isNotNull().isEqualTo(expectedValue); + return this; + } + + Verifier assertThatLeakValueIs(Metric metric, double expectedValue) { + TestContext context = run(metric, true); + assertThat(context.doubleLeakValue).isNotNull().isEqualTo(expectedValue); + return this; + } + + Verifier assertThatLeakValueIs(Metric metric, Rating expectedRating) { + TestContext context = run(metric, true); + assertThat(context.ratingLeakValue).isNotNull().isEqualTo(expectedRating); + return this; + } + + Verifier assertThatValueIs(Metric metric, Rating expectedValue) { + TestContext context = run(metric, false); + assertThat(context.ratingValue).isNotNull().isEqualTo(expectedValue); + return this; + } + + private TestContext run(Metric metric, boolean expectLeakFormula) { + IssueMetricFormula formula = underTest.getFormulas().stream() + .filter(f -> f.getMetric().getKey().equals(metric.getKey())) + .findFirst() + .get(); + assertThat(formula.isOnLeak()).isEqualTo(expectLeakFormula); + TestContext context = new TestContext(formula.getDependentMetrics(), values, leakValues); + formula.compute(context, newIssueCounter(groups)); + return context; + } + } + + private static IssueCounter newIssueCounter(IssueGroupDto... issues) { + return new IssueCounter(asList(issues)); + } + + private static IssueGroupDto newGroup() { + return newGroup(RuleType.CODE_SMELL); + } + + private static IssueGroupDto newGroup(RuleType ruleType) { + IssueGroupDto dto = new IssueGroupDto(); + // set non-null fields + dto.setRuleType(ruleType.getDbConstant()); + dto.setCount(1); + dto.setEffort(0.0); + dto.setSeverity(Severity.INFO); + dto.setStatus(Issue.STATUS_OPEN); + dto.setInLeak(false); + return dto; + } + + private static IssueGroupDto newResolvedGroup(RuleType ruleType) { + return newGroup(ruleType).setResolution(Issue.RESOLUTION_FALSE_POSITIVE).setStatus(Issue.STATUS_CLOSED); + } + + private static IssueGroupDto newResolvedGroup(String resolution, String status) { + return newGroup().setResolution(resolution).setStatus(status); + } + + private static class TestContext implements IssueMetricFormula.Context { + private final Set dependentMetrics; + private Double doubleValue; + private Rating ratingValue; + private Double doubleLeakValue; + private Rating ratingLeakValue; + private final Map values; + private final Map leakValues; + + private TestContext(Collection dependentMetrics, Map values, Map leakValues) { + this.dependentMetrics = new HashSet<>(dependentMetrics); + this.values = values; + this.leakValues = leakValues; + } + + @Override + public ComponentDto getComponent() { + throw new UnsupportedOperationException(); + } + + @Override + public DebtRatingGrid getDebtRatingGrid() { + return new DebtRatingGrid(new double[] {0.05, 0.1, 0.2, 0.5}); + } + + @Override + public Optional getValue(Metric metric) { + if (!dependentMetrics.contains(metric)) { + throw new IllegalStateException("Metric " + metric.getKey() + " is not declared as a dependency"); + } + if (values.containsKey(metric)) { + return Optional.of(values.get(metric)); + } + return Optional.empty(); + } + + @Override + public Optional getLeakValue(Metric metric) { + if (!dependentMetrics.contains(metric)) { + throw new IllegalStateException("Metric " + metric.getKey() + " is not declared as a dependency"); + } + if (leakValues.containsKey(metric)) { + return Optional.of(leakValues.get(metric)); + } + return Optional.empty(); + } + + @Override + public void setValue(double value) { + this.doubleValue = value; + } + + @Override + public void setValue(Rating value) { + this.ratingValue = value; + } + + @Override + public void setLeakValue(double value) { + this.doubleLeakValue = value; + } + + @Override + public void setLeakValue(Rating value) { + this.ratingLeakValue = value; + } + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/live/LiveMeasureComputerImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/live/LiveMeasureComputerImplTest.java new file mode 100644 index 00000000000..be76582d739 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/live/LiveMeasureComputerImplTest.java @@ -0,0 +1,377 @@ +/* + * 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.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.Mockito; +import org.sonar.api.config.PropertyDefinitions; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.api.measures.Metric; +import org.sonar.api.resources.Qualifiers; +import org.sonar.core.config.CorePropertyDefinitions; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ComponentTesting; +import org.sonar.db.measure.LiveMeasureDto; +import org.sonar.db.metric.MetricDto; +import org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating; +import org.sonar.server.es.ProjectIndexer; +import org.sonar.server.es.TestProjectIndexers; +import org.sonar.server.qualitygate.changeevent.QGChangeEvent; +import org.sonar.server.settings.ProjectConfigurationLoader; +import org.sonar.server.settings.TestProjectConfigurationLoader; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singleton; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.sonar.api.resources.Qualifiers.ORDERED_BOTTOM_UP; + +public class LiveMeasureComputerImplTest { + + @Rule + public DbTester db = DbTester.create(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private TestProjectIndexers projectIndexer = new TestProjectIndexers(); + private MetricDto intMetric; + private MetricDto ratingMetric; + private ComponentDto project; + private ComponentDto dir; + private ComponentDto file1; + private ComponentDto file2; + + @Before + public void setUp() throws Exception { + intMetric = db.measures().insertMetric(m -> m.setValueType(Metric.ValueType.INT.name())); + ratingMetric = db.measures().insertMetric(m -> m.setValueType(Metric.ValueType.RATING.name())); + project = db.components().insertMainBranch(); + dir = db.components().insertComponent(ComponentTesting.newDirectory(project, "src/main/java")); + file1 = db.components().insertComponent(ComponentTesting.newFileDto(project, dir)); + file2 = db.components().insertComponent(ComponentTesting.newFileDto(project, dir)); + } + + @Test + public void compute_and_insert_measures_if_they_dont_exist_yet() { + markProjectAsAnalyzed(project); + + List result = run(asList(file1, file2), newQualifierBasedIntFormula(), newRatingConstantFormula(Rating.C)); + + // 2 measures per component have been created + // Numeric value depends on qualifier (see newQualifierBasedIntFormula()) + assertThat(db.countRowsOfTable(db.getSession(), "live_measures")).isEqualTo(8); + assertThatIntMeasureHasValue(file1, ORDERED_BOTTOM_UP.indexOf(Qualifiers.FILE)); + assertThatRatingMeasureHasValue(file1, Rating.C); + assertThatIntMeasureHasValue(file2, ORDERED_BOTTOM_UP.indexOf(Qualifiers.FILE)); + assertThatRatingMeasureHasValue(file2, Rating.C); + assertThatIntMeasureHasValue(dir, ORDERED_BOTTOM_UP.indexOf(Qualifiers.DIRECTORY)); + assertThatRatingMeasureHasValue(dir, Rating.C); + assertThatIntMeasureHasValue(project, ORDERED_BOTTOM_UP.indexOf(Qualifiers.PROJECT)); + assertThatRatingMeasureHasValue(project, Rating.C); + assertThatProjectChanged(result, project); + } + + @Test + public void compute_and_update_measures_if_they_already_exist() { + markProjectAsAnalyzed(project); + db.measures().insertLiveMeasure(project, intMetric, m -> m.setValue(42.0)); + db.measures().insertLiveMeasure(dir, intMetric, m -> m.setValue(42.0)); + db.measures().insertLiveMeasure(file1, intMetric, m -> m.setValue(42.0)); + db.measures().insertLiveMeasure(file2, intMetric, m -> m.setValue(42.0)); + + // generates values 1, 2, 3 + List result = run(file1, newQualifierBasedIntFormula()); + + assertThat(db.countRowsOfTable(db.getSession(), "live_measures")).isEqualTo(4); + assertThatProjectChanged(result, project); + + // Numeric value depends on qualifier (see newQualifierBasedIntFormula()) + assertThatIntMeasureHasValue(file1, ORDERED_BOTTOM_UP.indexOf(Qualifiers.FILE)); + assertThatIntMeasureHasValue(dir, ORDERED_BOTTOM_UP.indexOf(Qualifiers.DIRECTORY)); + assertThatIntMeasureHasValue(project, ORDERED_BOTTOM_UP.indexOf(Qualifiers.PROJECT)); + // untouched + assertThatIntMeasureHasValue(file2, 42.0); + } + + @Test + public void variation_is_refreshed_when_int_value_is_changed() { + markProjectAsAnalyzed(project); + // value is: + // 42 on last analysis + // 42-12=30 on beginning of leak period + db.measures().insertLiveMeasure(project, intMetric, m -> m.setValue(42.0).setVariation(12.0)); + + // new value is 44, so variation on leak period is 44-30=14 + List result = run(file1, newIntConstantFormula(44.0)); + + LiveMeasureDto measure = assertThatIntMeasureHasValue(project, 44.0); + assertThat(measure.getVariation()).isEqualTo(14.0); + assertThatProjectChanged(result, project); + } + + @Test + public void variation_is_refreshed_when_rating_value_is_changed() { + markProjectAsAnalyzed(project); + // value is: + // B on last analysis + // D on beginning of leak period --> variation is -2 + db.measures().insertLiveMeasure(project, ratingMetric, m -> m.setValue((double) Rating.B.getIndex()).setData("B").setVariation(-2.0)); + + // new value is C, so variation on leak period is D to C = -1 + List result = run(file1, newRatingConstantFormula(Rating.C)); + + LiveMeasureDto measure = assertThatRatingMeasureHasValue(project, Rating.C); + assertThat(measure.getVariation()).isEqualTo(-1.0); + assertThatProjectChanged(result, project); + } + + @Test + public void variation_does_not_change_if_rating_value_does_not_change() { + markProjectAsAnalyzed(project); + // value is: + // B on last analysis + // D on beginning of leak period --> variation is -2 + db.measures().insertLiveMeasure(project, ratingMetric, m -> m.setValue((double) Rating.B.getIndex()).setData("B").setVariation(-2.0)); + + // new value is still B, so variation on leak period is still -2 + List result = run(file1, newRatingConstantFormula(Rating.B)); + + LiveMeasureDto measure = assertThatRatingMeasureHasValue(project, Rating.B); + assertThat(measure.getVariation()).isEqualTo(-2.0); + assertThatProjectChanged(result, project); + } + + @Test + public void refresh_leak_measures() { + markProjectAsAnalyzed(project); + db.measures().insertLiveMeasure(project, intMetric, m -> m.setVariation(42.0).setValue(null)); + db.measures().insertLiveMeasure(project, ratingMetric, m -> m.setVariation((double)Rating.E.getIndex())); + db.measures().insertLiveMeasure(dir, intMetric, m -> m.setVariation(42.0).setValue(null)); + db.measures().insertLiveMeasure(dir, ratingMetric, m -> m.setVariation((double)Rating.D.getIndex())); + db.measures().insertLiveMeasure(file1, intMetric, m -> m.setVariation(42.0).setValue(null)); + db.measures().insertLiveMeasure(file1, ratingMetric, m -> m.setVariation((double)Rating.C.getIndex())); + + // generates values 1, 2, 3 on leak measures + List result = run(file1, newQualifierBasedIntLeakFormula(), newRatingLeakFormula(Rating.B)); + + assertThat(db.countRowsOfTable(db.getSession(), "live_measures")).isEqualTo(6); + + // Numeric value depends on qualifier (see newQualifierBasedIntLeakFormula()) + assertThatIntMeasureHasLeakValue(file1, ORDERED_BOTTOM_UP.indexOf(Qualifiers.FILE)); + assertThatRatingMeasureHasLeakValue(file1, Rating.B); + assertThatIntMeasureHasLeakValue(dir, ORDERED_BOTTOM_UP.indexOf(Qualifiers.DIRECTORY)); + assertThatRatingMeasureHasLeakValue(dir, Rating.B); + assertThatIntMeasureHasLeakValue(project, ORDERED_BOTTOM_UP.indexOf(Qualifiers.PROJECT)); + assertThatRatingMeasureHasLeakValue(project, Rating.B); + assertThatProjectChanged(result, project); + } + + @Test + public void do_nothing_if_project_has_not_being_analyzed() { + // project has no snapshots + List result = run(file1, newIncrementalFormula()); + + assertThat(db.countRowsOfTable(db.getSession(), "live_measures")).isEqualTo(0); + assertThatProjectNotChanged(result, project); + } + + @Test + public void do_nothing_if_input_components_are_empty() { + List result = run(emptyList(), newIncrementalFormula()); + + assertThat(db.countRowsOfTable(db.getSession(), "live_measures")).isEqualTo(0); + assertThatProjectNotChanged(result, project); + } + + @Test + public void refresh_multiple_projects_at_the_same_time() { + markProjectAsAnalyzed(project); + ComponentDto project2 = db.components().insertMainBranch(); + ComponentDto fileInProject2 = db.components().insertComponent(ComponentTesting.newFileDto(project2)); + markProjectAsAnalyzed(project2); + + List result = run(asList(file1, fileInProject2), newQualifierBasedIntFormula()); + + // generated values depend on position of qualifier in Qualifiers.ORDERED_BOTTOM_UP (see formula) + assertThatIntMeasureHasValue(file1, 0); + assertThatIntMeasureHasValue(dir, 2); + assertThatIntMeasureHasValue(project, 4); + assertThatIntMeasureHasValue(fileInProject2, 0); + assertThatIntMeasureHasValue(project2, 4); + + // no other measures generated + assertThat(db.countRowsOfTable(db.getSession(), "live_measures")).isEqualTo(5); + assertThatProjectChanged(result, project, project2); + } + + @Test + public void refresh_multiple_branches_at_the_same_time() { + // FIXME + } + + @Test + public void compute_quality_gate_status() { + // FIXME + } + + @Test + public void exception_describes_context_when_a_formula_fails() { + markProjectAsAnalyzed(project); + Metric metric = new Metric.Builder(intMetric.getKey(), intMetric.getShortName(), Metric.ValueType.valueOf(intMetric.getValueType())).create(); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Fail to compute " + metric.getKey() + " on " + project.getDbKey()); + + run(project, new IssueMetricFormula(metric, false, (context, issueCounter) -> { + throw new NullPointerException("BOOM"); + })); + } + + private List run(ComponentDto component, IssueMetricFormula... formulas) { + return run(singleton(component), formulas); + } + + private List run(Collection components, IssueMetricFormula... formulas) { + IssueMetricFormulaFactory formulaFactory = new TestIssueMetricFormulaFactory(asList(formulas)); + + LiveQualityGateComputer qGateComputer = mock(LiveQualityGateComputer.class, Mockito.RETURNS_DEEP_STUBS); + MapSettings settings = new MapSettings(new PropertyDefinitions(CorePropertyDefinitions.all())); + ProjectConfigurationLoader configurationLoader = new TestProjectConfigurationLoader(settings.asConfig()); + + LiveMeasureComputerImpl underTest = new LiveMeasureComputerImpl(db.getDbClient(), formulaFactory, qGateComputer, configurationLoader, projectIndexer); + + return underTest.refresh(db.getSession(), components); + } + + private void markProjectAsAnalyzed(ComponentDto p) { + assertThat(p.qualifier()).isEqualTo(Qualifiers.PROJECT); + db.components().insertSnapshot(p, s -> s.setPeriodDate(1_490_000_000L)); + } + + private LiveMeasureDto assertThatIntMeasureHasValue(ComponentDto component, double expectedValue) { + LiveMeasureDto measure = db.getDbClient().liveMeasureDao().selectMeasure(db.getSession(), component.uuid(), intMetric.getKey()).get(); + assertThat(measure.getComponentUuid()).isEqualTo(component.uuid()); + assertThat(measure.getProjectUuid()).isEqualTo(component.projectUuid()); + assertThat(measure.getMetricId()).isEqualTo(intMetric.getId()); + assertThat(measure.getValue()).isEqualTo(expectedValue); + return measure; + } + + private LiveMeasureDto assertThatRatingMeasureHasValue(ComponentDto component, Rating expectedRating) { + LiveMeasureDto measure = db.getDbClient().liveMeasureDao().selectMeasure(db.getSession(), component.uuid(), ratingMetric.getKey()).get(); + assertThat(measure.getComponentUuid()).isEqualTo(component.uuid()); + assertThat(measure.getProjectUuid()).isEqualTo(component.projectUuid()); + assertThat(measure.getMetricId()).isEqualTo(ratingMetric.getId()); + assertThat(measure.getValue()).isEqualTo(expectedRating.getIndex()); + assertThat(measure.getDataAsString()).isEqualTo(expectedRating.name()); + return measure; + } + + private void assertThatIntMeasureHasLeakValue(ComponentDto component, double expectedValue) { + LiveMeasureDto measure = db.getDbClient().liveMeasureDao().selectMeasure(db.getSession(), component.uuid(), intMetric.getKey()).get(); + assertThat(measure.getComponentUuid()).isEqualTo(component.uuid()); + assertThat(measure.getProjectUuid()).isEqualTo(component.projectUuid()); + assertThat(measure.getMetricId()).isEqualTo(intMetric.getId()); + assertThat(measure.getValue()).isNull(); + assertThat(measure.getVariation()).isEqualTo(expectedValue); + } + + private void assertThatRatingMeasureHasLeakValue(ComponentDto component, Rating expectedValue) { + LiveMeasureDto measure = db.getDbClient().liveMeasureDao().selectMeasure(db.getSession(), component.uuid(), ratingMetric.getKey()).get(); + assertThat(measure.getComponentUuid()).isEqualTo(component.uuid()); + assertThat(measure.getProjectUuid()).isEqualTo(component.projectUuid()); + assertThat(measure.getMetricId()).isEqualTo(ratingMetric.getId()); + assertThat(measure.getVariation()).isEqualTo((double) expectedValue.getIndex()); + } + + private IssueMetricFormula newIncrementalFormula() { + Metric metric = new Metric.Builder(intMetric.getKey(), intMetric.getShortName(), Metric.ValueType.valueOf(intMetric.getValueType())).create(); + AtomicInteger counter = new AtomicInteger(); + return new IssueMetricFormula(metric, false, (ctx, issues) -> { + ctx.setValue((double) counter.incrementAndGet()); + }); + } + + private IssueMetricFormula newIntConstantFormula(double constant) { + Metric metric = new Metric.Builder(intMetric.getKey(), intMetric.getShortName(), Metric.ValueType.valueOf(intMetric.getValueType())).create(); + return new IssueMetricFormula(metric, false, (ctx, issues) -> { + ctx.setValue(constant); + }); + } + + private IssueMetricFormula newRatingConstantFormula(Rating constant) { + Metric metric = new Metric.Builder(ratingMetric.getKey(), ratingMetric.getShortName(), Metric.ValueType.valueOf(ratingMetric.getValueType())).create(); + return new IssueMetricFormula(metric, false, (ctx, issues) -> { + ctx.setValue(constant); + }); + } + + private IssueMetricFormula newIncrementalLeakFormula() { + Metric metric = new Metric.Builder(intMetric.getKey(), intMetric.getShortName(), Metric.ValueType.valueOf(intMetric.getValueType())).create(); + AtomicInteger counter = new AtomicInteger(); + return new IssueMetricFormula(metric, true, (ctx, issues) -> { + ctx.setLeakValue((double) counter.incrementAndGet()); + }); + } + + private IssueMetricFormula newRatingLeakFormula(Rating rating) { + Metric metric = new Metric.Builder(ratingMetric.getKey(), ratingMetric.getShortName(), Metric.ValueType.valueOf(ratingMetric.getValueType())).create(); + return new IssueMetricFormula(metric, true, (ctx, issues) -> { + ctx.setLeakValue(rating); + }); + } + + private IssueMetricFormula newQualifierBasedIntFormula() { + Metric metric = new Metric.Builder(intMetric.getKey(), intMetric.getShortName(), Metric.ValueType.valueOf(intMetric.getValueType())).create(); + return new IssueMetricFormula(metric, false, (ctx, issues) -> { + ctx.setValue(ORDERED_BOTTOM_UP.indexOf(ctx.getComponent().qualifier())); + }); + } + + private IssueMetricFormula newQualifierBasedIntLeakFormula() { + Metric metric = new Metric.Builder(intMetric.getKey(), intMetric.getShortName(), Metric.ValueType.valueOf(intMetric.getValueType())).create(); + return new IssueMetricFormula(metric, true, (ctx, issues) -> { + ctx.setLeakValue(ORDERED_BOTTOM_UP.indexOf(ctx.getComponent().qualifier())); + }); + } + + private void assertThatProjectChanged(List events, ComponentDto... projects) { + for (ComponentDto p : projects) { + assertThat(projectIndexer.hasBeenCalled(p.uuid(), ProjectIndexer.Cause.MEASURE_CHANGE)).isTrue(); + } + + assertThat(events).extracting(e -> e.getProject().uuid()) + .containsExactlyInAnyOrder(Arrays.stream(projects).map(ComponentDto::uuid).toArray(String[]::new)); + } + + private void assertThatProjectNotChanged(List events, ComponentDto project) { + assertThat(projectIndexer.hasBeenCalled(project.uuid(), ProjectIndexer.Cause.MEASURE_CHANGE)).isFalse(); + assertThat(events).hasSize(0); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/live/LiveMeasureModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/live/LiveMeasureModuleTest.java new file mode 100644 index 00000000000..3ac75e013d1 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/live/LiveMeasureModuleTest.java @@ -0,0 +1,37 @@ +/* + * 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.junit.Test; +import org.sonar.core.platform.ComponentContainer; + +import static org.assertj.core.api.Assertions.assertThat; + +public class LiveMeasureModuleTest { + + @Test + public void verify_count_of_added_components() { + ComponentContainer container = new ComponentContainer(); + new LiveMeasureModule().configure(container); + assertThat(container.size()).isEqualTo(3 + 2); + } + + +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/live/LiveQualityGateComputerImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/live/LiveQualityGateComputerImplTest.java new file mode 100644 index 00000000000..945165d987a --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/live/LiveQualityGateComputerImplTest.java @@ -0,0 +1,198 @@ +/* + * 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.ImmutableSet; +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.measures.Metric; +import org.sonar.db.DbTester; +import org.sonar.db.component.BranchDto; +import org.sonar.db.component.BranchType; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ComponentTesting; +import org.sonar.db.measure.LiveMeasureDto; +import org.sonar.db.metric.MetricDto; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.db.qualitygate.QGateWithOrgDto; +import org.sonar.db.qualitygate.QualityGateConditionDto; +import org.sonar.server.qualitygate.Condition; +import org.sonar.server.qualitygate.EvaluatedCondition; +import org.sonar.server.qualitygate.EvaluatedQualityGate; +import org.sonar.server.qualitygate.QualityGate; +import org.sonar.server.qualitygate.QualityGateEvaluator; +import org.sonar.server.qualitygate.QualityGateFinder; +import org.sonar.server.qualitygate.ShortLivingBranchQualityGate; + +import static com.google.common.base.Preconditions.checkState; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singleton; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.groups.Tuple.tuple; +import static org.sonar.db.component.ComponentTesting.newBranchDto; +import static org.sonar.db.metric.MetricTesting.newMetricDto; +import static org.sonar.db.organization.OrganizationTesting.newOrganizationDto; + +public class LiveQualityGateComputerImplTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Rule + public DbTester db = DbTester.create(); + + private TestQualityGateEvaluator qualityGateEvaluator = new TestQualityGateEvaluator(); + private LiveQualityGateComputerImpl underTest = new LiveQualityGateComputerImpl(db.getDbClient(), new QualityGateFinder(db.getDbClient()), qualityGateEvaluator); + + @Test + public void loadQualityGate_returns_hardcoded_gate_for_short_living_branches() { + OrganizationDto organization = db.organizations().insert(); + ComponentDto project = db.components().insertPublicProject(organization); + BranchDto branch = newBranchDto(project).setBranchType(BranchType.SHORT); + db.components().insertProjectBranch(project, branch); + + QualityGate result = underTest.loadQualityGate(db.getSession(), organization, project, branch); + + assertThat(result).isSameAs(ShortLivingBranchQualityGate.GATE); + } + + @Test + public void loadQualityGate_on_long_branch_returns_organization_default_gate() { + OrganizationDto organization = db.organizations().insert(); + ComponentDto project = db.components().insertPublicProject(organization); + BranchDto branch = newBranchDto(project).setBranchType(BranchType.LONG); + db.components().insertProjectBranch(project, branch); + + MetricDto metric = db.measures().insertMetric(); + QGateWithOrgDto gate = db.qualityGates().insertQualityGate(organization); + db.qualityGates().setDefaultQualityGate(organization, gate); + QualityGateConditionDto leakCondition = db.qualityGates().addCondition(gate, metric, c -> c.setPeriod(1)); + QualityGateConditionDto absoluteCondition = db.qualityGates().addCondition(gate, metric, c -> c.setPeriod(null)); + + QualityGate result = underTest.loadQualityGate(db.getSession(), organization, project, branch); + + assertThat(result.getId()).isEqualTo("" + gate.getId()); + assertThat(result.getConditions()) + .extracting(Condition::getMetricKey, Condition::getOperator, c -> c.getErrorThreshold().get(), c -> c.getWarningThreshold().get(), Condition::isOnLeakPeriod) + .containsExactlyInAnyOrder( + tuple(metric.getKey(), Condition.Operator.fromDbValue(leakCondition.getOperator()), leakCondition.getErrorThreshold(), leakCondition.getWarningThreshold(), true), + tuple(metric.getKey(), Condition.Operator.fromDbValue(absoluteCondition.getOperator()), absoluteCondition.getErrorThreshold(), absoluteCondition.getWarningThreshold(), + false)); + } + + @Test + public void getMetricsRelatedTo() { + Condition condition = new Condition("metric1", Condition.Operator.EQUALS, "10", null, false); + QualityGate gate = new QualityGate("1", "foo", ImmutableSet.of(condition)); + + Set result = underTest.getMetricsRelatedTo(gate); + + assertThat(result).containsExactlyInAnyOrder( + // the metrics needed to compute the status of gate + condition.getMetricKey(), + // generated metrics + CoreMetrics.ALERT_STATUS_KEY, CoreMetrics.QUALITY_GATE_DETAILS_KEY); + } + + @Test + public void refreshGateStatus_generates_gate_related_measures() { + ComponentDto project = ComponentTesting.newPublicProjectDto(newOrganizationDto()); + MetricDto conditionMetric = newMetricDto(); + MetricDto statusMetric = newMetricDto().setKey(CoreMetrics.ALERT_STATUS_KEY); + MetricDto detailsMetric = newMetricDto().setKey(CoreMetrics.QUALITY_GATE_DETAILS_KEY); + Condition condition = new Condition(conditionMetric.getKey(), Condition.Operator.GREATER_THAN, "10", null, false); + QualityGate gate = new QualityGate("1", "foo", ImmutableSet.of(condition)); + MeasureMatrix matrix = new MeasureMatrix(singleton(project), asList(conditionMetric, statusMetric, detailsMetric), emptyList()); + + EvaluatedQualityGate result = underTest.refreshGateStatus(project, gate, matrix); + + QualityGateEvaluator.Measures measures = qualityGateEvaluator.getCalledMeasures(); + assertThat(measures.get(conditionMetric.getKey())).isEmpty(); + + assertThat(result.getStatus()).isEqualTo(Metric.Level.OK); + assertThat(result.getEvaluatedConditions()) + .extracting(EvaluatedCondition::getStatus) + .containsExactly(EvaluatedCondition.EvaluationStatus.OK); + assertThat(matrix.getMeasure(project, CoreMetrics.ALERT_STATUS_KEY).get().getDataAsString()).isEqualTo(Metric.Level.OK.name()); + assertThat(matrix.getMeasure(project, CoreMetrics.QUALITY_GATE_DETAILS_KEY).get().getDataAsString()) + .isNotEmpty() + // json format + .startsWith("{").endsWith("}"); + } + + @Test + public void refreshGateStatus_provides_measures_to_evaluator() { + ComponentDto project = ComponentTesting.newPublicProjectDto(newOrganizationDto()); + MetricDto numericMetric = newMetricDto().setValueType(Metric.ValueType.FLOAT.name()); + MetricDto stringMetric = newMetricDto().setValueType(Metric.ValueType.STRING.name()); + MetricDto statusMetric = newMetricDto().setKey(CoreMetrics.ALERT_STATUS_KEY); + MetricDto detailsMetric = newMetricDto().setKey(CoreMetrics.QUALITY_GATE_DETAILS_KEY); + QualityGate gate = new QualityGate("1", "foo", Collections.emptySet()); + LiveMeasureDto numericMeasure = new LiveMeasureDto().setMetricId(numericMetric.getId()).setValue(1.23).setVariation(4.56).setComponentUuid(project.uuid()); + LiveMeasureDto stringMeasure = new LiveMeasureDto().setMetricId(stringMetric.getId()).setData("bar").setComponentUuid(project.uuid()); + MeasureMatrix matrix = new MeasureMatrix(singleton(project), asList(statusMetric, detailsMetric, numericMetric, stringMetric), asList(numericMeasure, stringMeasure)); + + underTest.refreshGateStatus(project, gate, matrix); + + QualityGateEvaluator.Measures measures = qualityGateEvaluator.getCalledMeasures(); + + QualityGateEvaluator.Measure loadedStringMeasure = measures.get(stringMetric.getKey()).get(); + assertThat(loadedStringMeasure.getStringValue()).hasValue("bar"); + assertThat(loadedStringMeasure.getValue()).isEmpty(); + assertThat(loadedStringMeasure.getLeakValue()).isEmpty(); + assertThat(loadedStringMeasure.getType()).isEqualTo(Metric.ValueType.STRING); + + QualityGateEvaluator.Measure loadedNumericMeasure = measures.get(numericMetric.getKey()).get(); + assertThat(loadedNumericMeasure.getStringValue()).isEmpty(); + assertThat(loadedNumericMeasure.getValue()).hasValue(1.23); + assertThat(loadedNumericMeasure.getLeakValue()).hasValue(4.56); + assertThat(loadedNumericMeasure.getType()).isEqualTo(Metric.ValueType.FLOAT); + } + + private static class TestQualityGateEvaluator implements QualityGateEvaluator { + private Measures measures; + + @Override + public EvaluatedQualityGate evaluate(QualityGate gate, Measures measures) { + checkState(this.measures == null); + this.measures = measures; + EvaluatedQualityGate.Builder builder = EvaluatedQualityGate.newBuilder().setQualityGate(gate).setStatus(Metric.Level.OK); + for (Condition condition : gate.getConditions()) { + builder.addCondition(condition, EvaluatedCondition.EvaluationStatus.OK, "bar"); + } + return builder.build(); + } + + private Measures getCalledMeasures() { + return measures; + } + + @Override + public Set getMetricKeys(QualityGate gate) { + return gate.getConditions().stream().map(Condition::getMetricKey).collect(Collectors.toSet()); + } + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/live/MeasureMatrixTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/live/MeasureMatrixTest.java new file mode 100644 index 00000000000..a163dc2c64f --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/live/MeasureMatrixTest.java @@ -0,0 +1,209 @@ +/* + * 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.Optional; +import javax.annotation.Nullable; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ComponentTesting; +import org.sonar.db.measure.LiveMeasureDto; +import org.sonar.db.metric.MetricDto; +import org.sonar.db.organization.OrganizationDto; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.db.metric.MetricTesting.newMetricDto; +import static org.sonar.db.organization.OrganizationTesting.newOrganizationDto; + +public class MeasureMatrixTest { + + private static final OrganizationDto ORGANIZATION = newOrganizationDto(); + private static final ComponentDto PROJECT = ComponentTesting.newPublicProjectDto(ORGANIZATION); + private static final ComponentDto FILE = ComponentTesting.newFileDto(PROJECT); + private static final MetricDto METRIC_1 = newMetricDto().setId(100); + private static final MetricDto METRIC_2 = newMetricDto().setId(200); + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void getMetric() { + Collection metrics = asList(METRIC_1, METRIC_2); + + MeasureMatrix underTest = new MeasureMatrix(asList(PROJECT, FILE), metrics, new ArrayList<>()); + + assertThat(underTest.getMetric(METRIC_2.getId())).isSameAs(METRIC_2); + } + + @Test + public void getMetric_fails_if_metric_is_not_registered() { + Collection metrics = asList(METRIC_1); + MeasureMatrix underTest = new MeasureMatrix(asList(PROJECT, FILE), metrics, new ArrayList<>()); + + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("Metric with id " + METRIC_2.getId() + " not found"); + + underTest.getMetric(METRIC_2.getId()); + } + + @Test + public void getValue_returns_empty_if_measure_is_absent() { + MetricDto metric = newMetricDto(); + LiveMeasureDto measure = newMeasure(metric, PROJECT).setValue(null); + MeasureMatrix underTest = new MeasureMatrix(asList(PROJECT), asList(metric), asList(measure)); + + assertThat(underTest.getMeasure(FILE, metric.getKey())).isEmpty(); + } + + @Test + public void getMeasure_throws_IAE_if_metric_is_not_registered() { + MeasureMatrix underTest = new MeasureMatrix(asList(PROJECT), asList(METRIC_1), emptyList()); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Metric with key _missing_ is not registered"); + + underTest.getMeasure(PROJECT, "_missing_"); + } + + @Test + public void setValue_double_rounds_up_and_updates_value() { + MetricDto metric = newMetricDto().setDecimalScale(2); + LiveMeasureDto measure = newMeasure(metric, PROJECT).setValue(1.23); + MeasureMatrix underTest = new MeasureMatrix(asList(PROJECT), asList(metric), asList(measure)); + + underTest.setValue(PROJECT, metric.getKey(), 3.14159); + + assertThat(underTest.getMeasure(PROJECT, metric.getKey()).get().getValue()).isEqualTo(3.14); + assertThat(underTest.getChanged()).hasSize(1); + + underTest.setValue(PROJECT, metric.getKey(), 3.148); + verifyValue(underTest, PROJECT, metric, 3.15); + verifyVariation(underTest, PROJECT, metric, null); + } + + private void verifyValue(MeasureMatrix underTest, ComponentDto component, MetricDto metric, @Nullable Double expectedValue) { + Optional measure = underTest.getMeasure(component, metric.getKey()); + assertThat(measure).isPresent(); + assertThat(measure.get().getValue()).isEqualTo(expectedValue); + } + + private void verifyVariation(MeasureMatrix underTest, ComponentDto component, MetricDto metric, @Nullable Double expectedVariation) { + assertThat(underTest.getMeasure(component, metric.getKey()).get().getVariation()).isEqualTo(expectedVariation); + } + + @Test + public void setValue_double_does_nothing_if_value_is_unchanged() { + MetricDto metric = newMetricDto().setDecimalScale(2); + LiveMeasureDto measure = newMeasure(metric, PROJECT).setValue(3.14); + MeasureMatrix underTest = new MeasureMatrix(asList(PROJECT), asList(metric), asList(measure)); + + underTest.setValue(PROJECT, metric.getKey(), 3.14159); + + assertThat(underTest.getChanged()).isEmpty(); + verifyValue(underTest, PROJECT, metric, 3.14); + } + + @Test + public void setValue_double_updates_variation() { + MetricDto metric = newMetricDto().setDecimalScale(2); + LiveMeasureDto measure = newMeasure(metric, PROJECT).setValue(3.14).setVariation(1.14); + MeasureMatrix underTest = new MeasureMatrix(asList(PROJECT), asList(metric), asList(measure)); + + underTest.setValue(PROJECT, metric.getKey(), 3.56); + + assertThat(underTest.getChanged()).hasSize(1); + verifyValue(underTest, PROJECT, metric, 3.56); + verifyVariation(underTest, PROJECT, metric, 3.56 - (3.14 - 1.14)); + } + + @Test + public void setValue_double_rounds_up_variation() { + MetricDto metric = newMetricDto().setDecimalScale(2); + LiveMeasureDto measure = newMeasure(metric, PROJECT).setValue(3.14).setVariation(1.14); + MeasureMatrix underTest = new MeasureMatrix(asList(PROJECT), asList(metric), asList(measure)); + + underTest.setValue(PROJECT, metric.getKey(), 3.569); + + assertThat(underTest.getChanged()).hasSize(1); + verifyValue(underTest, PROJECT, metric, 3.57); + verifyVariation(underTest, PROJECT, metric, 1.57); + } + + @Test + public void setValue_String_does_nothing_if_value_is_not_changed() { + LiveMeasureDto measure = newMeasure(METRIC_1, PROJECT).setData("foo"); + MeasureMatrix underTest = new MeasureMatrix(asList(PROJECT, FILE), asList(METRIC_1), asList(measure)); + + underTest.setValue(PROJECT, METRIC_1.getKey(), "foo"); + + assertThat(underTest.getMeasure(PROJECT, METRIC_1.getKey()).get().getDataAsString()).isEqualTo("foo"); + assertThat(underTest.getChanged()).isEmpty(); + } + + @Test + public void setValue_String_updates_value() { + LiveMeasureDto measure = newMeasure(METRIC_1, PROJECT).setData("foo"); + MeasureMatrix underTest = new MeasureMatrix(asList(PROJECT, FILE), asList(METRIC_1), asList(measure)); + + underTest.setValue(PROJECT, METRIC_1.getKey(), "bar"); + + assertThat(underTest.getMeasure(PROJECT, METRIC_1.getKey()).get().getDataAsString()).isEqualTo("bar"); + assertThat(underTest.getChanged()).extracting(LiveMeasureDto::getDataAsString).containsExactly("bar"); + } + + @Test + public void setLeakValue_rounds_up_and_updates_value() { + MetricDto metric = newMetricDto().setDecimalScale(2); + LiveMeasureDto measure = newMeasure(metric, PROJECT).setValue(null); + MeasureMatrix underTest = new MeasureMatrix(asList(PROJECT), asList(metric), asList(measure)); + + underTest.setLeakValue(PROJECT, metric.getKey(), 3.14159); + verifyVariation(underTest, PROJECT, metric, 3.14); + // do not update value + verifyValue(underTest, PROJECT, metric, null); + + underTest.setLeakValue(PROJECT, metric.getKey(), 3.148); + verifyVariation(underTest, PROJECT, metric, 3.15); + // do not update value + verifyValue(underTest, PROJECT, metric, null); + } + + @Test + public void setLeakValue_double_does_nothing_if_value_is_unchanged() { + MetricDto metric = newMetricDto().setDecimalScale(2); + LiveMeasureDto measure = newMeasure(metric, PROJECT).setValue(null).setVariation(3.14); + MeasureMatrix underTest = new MeasureMatrix(asList(PROJECT), asList(metric), asList(measure)); + + underTest.setLeakValue(PROJECT, metric.getKey(), 3.14159); + + assertThat(underTest.getChanged()).isEmpty(); + verifyVariation(underTest, PROJECT, metric, 3.14); + } + + private LiveMeasureDto newMeasure(MetricDto metric, ComponentDto component) { + return new LiveMeasureDto().setMetricId(metric.getId()).setData("foo").setComponentUuid(component.uuid()); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/live/TestIssueMetricFormulaFactory.java b/server/sonar-server/src/test/java/org/sonar/server/measure/live/TestIssueMetricFormulaFactory.java new file mode 100644 index 00000000000..ee29a7ee327 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/live/TestIssueMetricFormulaFactory.java @@ -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.List; +import java.util.Set; +import org.sonar.api.measures.Metric; + +class TestIssueMetricFormulaFactory implements IssueMetricFormulaFactory { + + private final List formulas; + + TestIssueMetricFormulaFactory(List formulas) { + this.formulas = formulas; + } + + @Override + public List getFormulas() { + return formulas; + } + + @Override + public Set getFormulaMetrics() { + return IssueMetricFormulaFactory.extractMetrics(formulas); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualitygate/ConditionEvaluatorTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/ConditionEvaluatorTest.java new file mode 100644 index 00000000000..f8b4234992c --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/ConditionEvaluatorTest.java @@ -0,0 +1,436 @@ +/* + * 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 javax.annotation.Nullable; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.measures.Metric; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.server.qualitygate.ConditionEvaluatorTest.FakeMeasure.newFakeMeasureOnLeak; + +public class ConditionEvaluatorTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void EQUALS_double() { + test(new FakeMeasure(10.1d), Condition.Operator.EQUALS, "10.2", EvaluatedCondition.EvaluationStatus.OK, "10.1"); + test(new FakeMeasure(10.2d), Condition.Operator.EQUALS, "10.2", EvaluatedCondition.EvaluationStatus.ERROR, "10.2"); + test(new FakeMeasure(10.3d), Condition.Operator.EQUALS, "10.2", EvaluatedCondition.EvaluationStatus.OK, "10.3"); + } + + @Test + public void NOT_EQUALS_double() { + test(new FakeMeasure(10.1d), Condition.Operator.NOT_EQUALS, "10.2", EvaluatedCondition.EvaluationStatus.ERROR, "10.1"); + test(new FakeMeasure(10.2d), Condition.Operator.NOT_EQUALS, "10.2", EvaluatedCondition.EvaluationStatus.OK, "10.2"); + test(new FakeMeasure(10.3d), Condition.Operator.NOT_EQUALS, "10.2", EvaluatedCondition.EvaluationStatus.ERROR, "10.3"); + } + + @Test + public void GREATER_THAN_double() { + test(new FakeMeasure(10.1d), Condition.Operator.GREATER_THAN, "10.2", EvaluatedCondition.EvaluationStatus.OK, "10.1"); + test(new FakeMeasure(10.2d), Condition.Operator.GREATER_THAN, "10.2", EvaluatedCondition.EvaluationStatus.OK, "10.2"); + test(new FakeMeasure(10.3d), Condition.Operator.GREATER_THAN, "10.2", EvaluatedCondition.EvaluationStatus.ERROR, "10.3"); + } + + @Test + public void LESS_THAN_double() { + test(new FakeMeasure(10.1d), Condition.Operator.LESS_THAN, "10.2", EvaluatedCondition.EvaluationStatus.ERROR, "10.1"); + test(new FakeMeasure(10.2d), Condition.Operator.LESS_THAN, "10.2", EvaluatedCondition.EvaluationStatus.OK, "10.2"); + test(new FakeMeasure(10.3d), Condition.Operator.LESS_THAN, "10.2", EvaluatedCondition.EvaluationStatus.OK, "10.3"); + } + + @Test + public void EQUALS_int() { + test(new FakeMeasure(10), Condition.Operator.EQUALS, "9", EvaluatedCondition.EvaluationStatus.OK, "10"); + test(new FakeMeasure(10), Condition.Operator.EQUALS, "10", EvaluatedCondition.EvaluationStatus.ERROR, "10"); + test(new FakeMeasure(10), Condition.Operator.EQUALS, "11", EvaluatedCondition.EvaluationStatus.OK, "10"); + + // badly stored thresholds are truncated + test(new FakeMeasure(10), Condition.Operator.EQUALS, "10.4", EvaluatedCondition.EvaluationStatus.ERROR, "10"); + test(new FakeMeasure(10), Condition.Operator.EQUALS, "10.9", EvaluatedCondition.EvaluationStatus.ERROR, "10"); + test(new FakeMeasure(11), Condition.Operator.EQUALS, "10.9", EvaluatedCondition.EvaluationStatus.OK, "11"); + } + + @Test + public void NOT_EQUALS_int() { + test(new FakeMeasure(10), Condition.Operator.NOT_EQUALS, "9", EvaluatedCondition.EvaluationStatus.ERROR, "10"); + test(new FakeMeasure(10), Condition.Operator.NOT_EQUALS, "10", EvaluatedCondition.EvaluationStatus.OK, "10"); + test(new FakeMeasure(10), Condition.Operator.NOT_EQUALS, "11", EvaluatedCondition.EvaluationStatus.ERROR, "10"); + + // badly stored thresholds are truncated + test(new FakeMeasure(10), Condition.Operator.NOT_EQUALS, "10.4", EvaluatedCondition.EvaluationStatus.OK, "10"); + test(new FakeMeasure(10), Condition.Operator.NOT_EQUALS, "10.9", EvaluatedCondition.EvaluationStatus.OK, "10"); + test(new FakeMeasure(10), Condition.Operator.NOT_EQUALS, "9.9", EvaluatedCondition.EvaluationStatus.ERROR, "10"); + } + + @Test + public void GREATER_THAN_int() { + test(new FakeMeasure(10), Condition.Operator.GREATER_THAN, "9", EvaluatedCondition.EvaluationStatus.ERROR, "10"); + test(new FakeMeasure(10), Condition.Operator.GREATER_THAN, "10", EvaluatedCondition.EvaluationStatus.OK, "10"); + test(new FakeMeasure(10), Condition.Operator.GREATER_THAN, "11", EvaluatedCondition.EvaluationStatus.OK, "10"); + + testOnLeak(newFakeMeasureOnLeak(10), Condition.Operator.GREATER_THAN, "9", EvaluatedCondition.EvaluationStatus.ERROR, "10"); + testOnLeak(newFakeMeasureOnLeak(10), Condition.Operator.GREATER_THAN, "10", EvaluatedCondition.EvaluationStatus.OK, "10"); + testOnLeak(newFakeMeasureOnLeak(10), Condition.Operator.GREATER_THAN, "11", EvaluatedCondition.EvaluationStatus.OK, "10"); + } + + @Test + public void LESS_THAN_int() { + test(new FakeMeasure(10), Condition.Operator.LESS_THAN, "9", EvaluatedCondition.EvaluationStatus.OK, "10"); + test(new FakeMeasure(10), Condition.Operator.LESS_THAN, "10", EvaluatedCondition.EvaluationStatus.OK, "10"); + test(new FakeMeasure(10), Condition.Operator.LESS_THAN, "11", EvaluatedCondition.EvaluationStatus.ERROR, "10"); + test(new FakeMeasure(10), Condition.Operator.LESS_THAN, "11", EvaluatedCondition.EvaluationStatus.ERROR, "10"); + + testOnLeak(newFakeMeasureOnLeak(10), Condition.Operator.LESS_THAN, "9", EvaluatedCondition.EvaluationStatus.OK, "10"); + testOnLeak(newFakeMeasureOnLeak(10), Condition.Operator.LESS_THAN, "10", EvaluatedCondition.EvaluationStatus.OK, "10"); + testOnLeak(newFakeMeasureOnLeak(10), Condition.Operator.LESS_THAN, "11", EvaluatedCondition.EvaluationStatus.ERROR, "10"); + testOnLeak(newFakeMeasureOnLeak(10), Condition.Operator.LESS_THAN, "11", EvaluatedCondition.EvaluationStatus.ERROR, "10"); + } + + @Test + public void no_value_present() { + test(new FakeMeasure((Integer) null), Condition.Operator.LESS_THAN, "9", EvaluatedCondition.EvaluationStatus.OK, null); + test(null, Condition.Operator.LESS_THAN, "9", EvaluatedCondition.EvaluationStatus.OK, null); + } + + @Test + public void empty_warning_condition() { + test(new FakeMeasure(10), Condition.Operator.LESS_THAN, "9", null, EvaluatedCondition.EvaluationStatus.OK, "10"); + test(new FakeMeasure(10), Condition.Operator.LESS_THAN, "9", "", EvaluatedCondition.EvaluationStatus.OK, "10"); + test(new FakeMeasure(3), Condition.Operator.LESS_THAN, "9", "", EvaluatedCondition.EvaluationStatus.ERROR, "3"); + } + + private void test(@Nullable QualityGateEvaluator.Measure measure, Condition.Operator operator, String errorThreshold, EvaluatedCondition.EvaluationStatus expectedStatus, + @Nullable String expectedValue) { + test(measure, operator, errorThreshold, null, expectedStatus, expectedValue); + } + + private void test(@Nullable QualityGateEvaluator.Measure measure, Condition.Operator operator, String errorThreshold, @Nullable String warningThreshold, + EvaluatedCondition.EvaluationStatus expectedStatus, @Nullable String expectedValue) { + Condition condition = new Condition("foo", operator, errorThreshold, warningThreshold, false); + + EvaluatedCondition result = ConditionEvaluator.evaluate(condition, new FakeMeasures(measure)); + + assertThat(result.getStatus()).isEqualTo(expectedStatus); + if (expectedValue == null) { + assertThat(result.getValue()).isNotPresent(); + } else { + assertThat(result.getValue()).hasValue(expectedValue); + } + } + + private void testOnLeak(QualityGateEvaluator.Measure measure, Condition.Operator operator, String errorThreshold, EvaluatedCondition.EvaluationStatus expectedStatus, + @Nullable String expectedValue) { + Condition condition = new Condition("foo", operator, errorThreshold, null, true); + + EvaluatedCondition result = ConditionEvaluator.evaluate(condition, new FakeMeasures(measure)); + + assertThat(result.getStatus()).isEqualTo(expectedStatus); + if (expectedValue == null) { + assertThat(result.getValue()).isNotPresent(); + } else { + assertThat(result.getValue()).hasValue(expectedValue); + } + } + + private static class FakeMeasures implements QualityGateEvaluator.Measures { + private final QualityGateEvaluator.Measure measure; + + FakeMeasures(@Nullable QualityGateEvaluator.Measure measure) { + this.measure = measure; + } + + @Override + public Optional get(String metricKey) { + return Optional.ofNullable(measure); + } + } + + static class FakeMeasure implements QualityGateEvaluator.Measure { + private Double leakValue; + private Double value; + private Metric.ValueType valueType; + + private FakeMeasure() { + + } + + FakeMeasure(@Nullable Double value) { + this.value = value; + this.valueType = Metric.ValueType.FLOAT; + } + + FakeMeasure(@Nullable Integer value) { + this.value = value == null ? null : value.doubleValue(); + this.valueType = Metric.ValueType.INT; + } + + static FakeMeasure newFakeMeasureOnLeak(@Nullable Integer value) { + FakeMeasure that = new FakeMeasure(); + that.leakValue = value == null ? null : value.doubleValue(); + that.valueType = Metric.ValueType.INT; + return that; + } + + @Override + public Metric.ValueType getType() { + return valueType; + } + + @Override + public OptionalDouble getValue() { + return value == null ? OptionalDouble.empty() : OptionalDouble.of(value); + } + + @Override + public Optional getStringValue() { + return Optional.empty(); + } + + @Override + public OptionalDouble getLeakValue() { + return leakValue == null ? OptionalDouble.empty() : OptionalDouble.of(leakValue); + } + } + + // @Test + // public void testEquals_for_String() { + // Metric metric = createMetric(STRING); + // Measure measure = newMeasureBuilder().create("TEST"); + // + // EvaluationResultAssert.assertThat(underTest.evaluate(createErrorCondition(metric, EQUALS, "TEST"), + // measure)).hasLevel(ERROR).hasValue("TEST"); + // EvaluationResultAssert.assertThat(underTest.evaluate(createErrorCondition(metric, EQUALS, "TEST2"), + // measure)).hasLevel(OK).hasValue("TEST"); + // } + // + // @Test + // + // public void testNotEquals() { + // Metric metric = createMetric(STRING); + // Measure measure = newMeasureBuilder().create("TEST"); + // + // EvaluationResultAssert.assertThat(underTest.evaluate(createErrorCondition(metric, NOT_EQUALS, "TEST"), + // measure)).hasLevel(OK).hasValue("TEST"); + // EvaluationResultAssert.assertThat(underTest.evaluate(createErrorCondition(metric, NOT_EQUALS, "TEST2"), + // measure)).hasLevel(ERROR).hasValue("TEST"); + // } + // + // @Test + // public void testEquals_Percent() { + // Metric metric = createMetric(PERCENT); + // Measure measure = newMeasureBuilder().create(10.2d, 1, null); + // + // EvaluationResultAssert.assertThat(underTest.evaluate(createErrorCondition(metric, EQUALS, "10.2"), + // measure)).hasLevel(ERROR).hasValue(10.2d); + // } + // + // @Test + // public void testEquals_Float() { + // Metric metric = createMetric(PERCENT); + // Measure measure = newMeasureBuilder().create(10.2d, 1, null); + // + // EvaluationResultAssert.assertThat(underTest.evaluate(createErrorCondition(metric, EQUALS, "10.2"), + // measure)).hasLevel(ERROR).hasValue(10.2d); + // } + // + // @Test + // public void testEquals_Int() { + // Metric metric = createMetric(INT); + // Measure measure = newMeasureBuilder().create(10, null); + // + // EvaluationResultAssert.assertThat(underTest.evaluate(createErrorCondition(metric, EQUALS, "10"), measure)).hasLevel(ERROR).hasValue(10); + // EvaluationResultAssert.assertThat(underTest.evaluate(createErrorCondition(metric, EQUALS, "10.2"), + // measure)).hasLevel(ERROR).hasValue(10); + // } + // + // @Test + // public void testEquals_Level() { + // Metric metric = createMetric(LEVEL); + // Measure measure = newMeasureBuilder().create(ERROR); + // + // EvaluationResultAssert.assertThat(underTest.evaluate(createErrorCondition(metric, EQUALS, ERROR.name()), + // measure)).hasLevel(ERROR).hasValue(ERROR.name()); + // + // EvaluationResultAssert.assertThat(underTest.evaluate(createErrorCondition(metric, EQUALS, OK.name()), + // measure)).hasLevel(OK).hasValue(ERROR.name()); + // } + // + // @Test + // public void testNotEquals_Level() { + // Metric metric = createMetric(LEVEL); + // Measure measure = newMeasureBuilder().create(ERROR); + // + // EvaluationResultAssert.assertThat(underTest.evaluate(createErrorCondition(metric, NOT_EQUALS, OK.name()), + // measure)).hasLevel(ERROR).hasValue(ERROR.name()); + // } + // + // @Test + // public void testEquals_BOOL() { + // Metric metric = createMetric(BOOL); + // Measure measure = newMeasureBuilder().create(false, null); + // + // EvaluationResultAssert.assertThat(underTest.evaluate(createErrorCondition(metric, EQUALS, "1"), measure)).hasLevel(OK).hasValue(false); + // EvaluationResultAssert.assertThat(underTest.evaluate(createErrorCondition(metric, EQUALS, "0"), + // measure)).hasLevel(ERROR).hasValue(false); + // } + // + // @Test + // public void testNotEquals_BOOL() { + // Metric metric = createMetric(BOOL); + // Measure measure = newMeasureBuilder().create(false, null); + // + // EvaluationResultAssert.assertThat(underTest.evaluate(createErrorCondition(metric, NOT_EQUALS, "1"), + // measure)).hasLevel(ERROR).hasValue(false); + // EvaluationResultAssert.assertThat(underTest.evaluate(createErrorCondition(metric, NOT_EQUALS, "0"), + // measure)).hasLevel(OK).hasValue(false); + // } + // + // @Test + // public void getLevel_throws_IEA_if_error_threshold_is_not_parsable_boolean() { + // Metric metric = createMetric(BOOL); + // Measure measure = newMeasureBuilder().create(false, null); + // + // expectedException.expect(IllegalArgumentException.class); + // expectedException.expectMessage("Quality Gate: Unable to parse value 'polop' to compare against name"); + // + // underTest.evaluate(createErrorCondition(metric, EQUALS, "polop"), measure); + // } + // + // @Test + // public void testEquals_work_duration() { + // Metric metric = createMetric(WORK_DUR); + // Measure measure = newMeasureBuilder().create(60l, null); + // + // EvaluationResultAssert.assertThat(underTest.evaluate(createErrorCondition(metric, EQUALS, "60"), measure)).hasLevel(ERROR); + // } + // + // @Test + // public void getLevel_throws_IEA_if_error_threshold_is_not_parsable_long() { + // Metric metric = createMetric(WORK_DUR); + // Measure measure = newMeasureBuilder().create(60l, null); + // + // expectedException.expect(IllegalArgumentException.class); + // expectedException.expectMessage("Quality Gate: Unable to parse value 'polop' to compare against name"); + // + // underTest.evaluate(createErrorCondition(metric, EQUALS, "polop"), measure); + // } + // + // @Test + // public void testErrorAndWarningLevel() { + // Metric metric = createMetric(FLOAT); + // Measure measure = newMeasureBuilder().create(10.2d, 1, null); + // + // EvaluationResultAssert.assertThat(underTest.evaluate(createErrorCondition(metric, EQUALS, "10.2"), measure)).hasLevel(ERROR); + // EvaluationResultAssert.assertThat(underTest.evaluate(createErrorCondition(metric, EQUALS, "10.1"), measure)).hasLevel(OK); + // + // EvaluationResultAssert.assertThat(underTest.evaluate(new org.sonar.server.computation.task.projectanalysis.qualitygate.Condition(metric, + // EQUALS.getDbValue(), "10.3", "10.2", false), measure)).hasLevel(Measure.Level.WARN); + // EvaluationResultAssert.assertThat(underTest.evaluate(new org.sonar.server.computation.task.projectanalysis.qualitygate.Condition(metric, + // LESS_THAN.getDbValue(), "10.3", "10.2", false), measure)).hasLevel(Measure.Level.ERROR); + // } + // + // @Test + // public void condition_is_always_ok_when_measure_is_noValue() { + // for (Metric.MetricType metricType : from(asList(values())).filter(not(in(ImmutableSet.of(DATA, LEVEL))))) { + // Metric metric = createMetric(metricType); + // Measure measure = newMeasureBuilder().createNoValue(); + // + // EvaluationResultAssert.assertThat(underTest.evaluate(createErrorCondition(metric, EQUALS, "10.2"), measure)).hasLevel(OK); + // } + // } + // + // @Test + // public void testUnsupportedType() { + // Metric metric = createMetric(DATA); + // Measure measure = newMeasureBuilder().create("3.14159265358"); + // + // expectedException.expect(IllegalArgumentException.class); + // expectedException.expectMessage("Conditions on MetricType DATA are not supported"); + // + // underTest.evaluate(createErrorCondition(metric, EQUALS, "1.60217657"), measure); + // } + // + // @Test + // public void condition_on_period() { + // for (Metric.MetricType metricType : ImmutableList.of(FLOAT, INT, WORK_DUR)) { + // Metric metric = createMetric(metricType); + // Measure measure = newMeasureBuilder().setVariation(3d).createNoValue(); + // + // EvaluationResultAssert.assertThat(underTest.evaluate(new org.sonar.server.computation.task.projectanalysis.qualitygate.Condition(metric, + // GREATER_THAN.getDbValue(), "3", null, true), measure)).hasLevel(OK); + // } + // } + // + // @Test + // public void condition_on_period_without_value_is_OK() { + // Metric metric = createMetric(FLOAT); + // Measure measure = newMeasureBuilder().createNoValue(); + // + // EvaluationResultAssert.assertThat(underTest.evaluate(new org.sonar.server.computation.task.projectanalysis.qualitygate.Condition(metric, + // GREATER_THAN.getDbValue(), "3", null, true), measure)).hasLevel(OK).hasValue(null); + // } + // + // @Test + // public void condition_on_rating() throws Exception { + // Metric metric = createMetric(RATING); + // Measure measure = newMeasureBuilder().create(4, "D"); + // + // EvaluationResultAssert.assertThat(underTest.evaluate(new org.sonar.server.computation.task.projectanalysis.qualitygate.Condition(metric, + // GREATER_THAN.getDbValue(), "4", null, false), measure)).hasLevel(OK).hasValue(4); + // EvaluationResultAssert.assertThat(underTest.evaluate(new org.sonar.server.computation.task.projectanalysis.qualitygate.Condition(metric, + // GREATER_THAN.getDbValue(), "2", null, false), measure)).hasLevel(ERROR).hasValue(4); + // } + // + // @Test + // public void condition_on_rating_on_leak_period() throws Exception { + // Metric metric = createMetric(RATING); + // Measure measure = newMeasureBuilder().setVariation(4d).createNoValue(); + // + // EvaluationResultAssert.assertThat(underTest.evaluate(new org.sonar.server.computation.task.projectanalysis.qualitygate.Condition(metric, + // GREATER_THAN.getDbValue(), "5", null, true), measure)).hasLevel(OK).hasValue(4); + // EvaluationResultAssert.assertThat(underTest.evaluate(new org.sonar.server.computation.task.projectanalysis.qualitygate.Condition(metric, + // GREATER_THAN.getDbValue(), "2", null, true), measure)).hasLevel(ERROR).hasValue(4); + // } + // + // @Test + // public void condition_on_rating_on_leak_period_when_variation_is_zero() throws Exception { + // Metric metric = createMetric(RATING); + // Measure measure = newMeasureBuilder().setVariation(0d).createNoValue(); + // + // EvaluationResultAssert.assertThat(underTest.evaluate(new org.sonar.server.computation.task.projectanalysis.qualitygate.Condition(metric, + // GREATER_THAN.getDbValue(), "4", null, true), measure)).hasLevel(OK).hasValue(0); + // } + // + // private static org.sonar.server.computation.task.projectanalysis.qualitygate.Condition createErrorCondition(Metric metric, + // org.sonar.server.computation.task.projectanalysis.qualitygate.Condition.Operator operator, String errorThreshold) { + // return new Condition(metric, operator.getDbValue(), errorThreshold, null, false); + // } + // + // private static MetricImpl createMetric(Metric.MetricType metricType) { + // return new MetricImpl(1, "key", "name", metricType); + // } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualitygate/EvaluatedQualityGateTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/EvaluatedQualityGateTest.java index 9c55e05e952..b02d5dd2cd5 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/qualitygate/EvaluatedQualityGateTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/EvaluatedQualityGateTest.java @@ -25,13 +25,12 @@ import org.apache.commons.lang.RandomStringUtils; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.sonar.api.measures.Metric.Level; import static java.util.Collections.emptySet; import static java.util.Collections.singleton; import static org.assertj.core.api.Assertions.assertThat; import static org.sonar.server.qualitygate.EvaluatedQualityGate.newBuilder; -import static org.sonar.server.qualitygate.EvaluatedQualityGate.Status.OK; -import static org.sonar.server.qualitygate.EvaluatedQualityGate.Status.WARN; public class EvaluatedQualityGateTest { private static final String QUALITY_GATE_ID = "qg_id"; @@ -45,37 +44,13 @@ public class EvaluatedQualityGateTest { public ExpectedException expectedException = ExpectedException.none(); private final Random random = new Random(); - private final EvaluatedQualityGate.Status randomStatus = EvaluatedQualityGate.Status.values()[random.nextInt(EvaluatedQualityGate.Status.values().length)]; + private final Level randomStatus = Level.values()[random.nextInt(Level.values().length)]; private final EvaluatedCondition.EvaluationStatus randomEvaluationStatus = EvaluatedCondition.EvaluationStatus.values()[random .nextInt(EvaluatedCondition.EvaluationStatus.values().length)]; private final String randomValue = random.nextBoolean() ? null : RandomStringUtils.randomAlphanumeric(3); private EvaluatedQualityGate.Builder builder = newBuilder(); - @Test - public void setQualityGate_fails_with_NPE_if_argument_is_null() { - expectedException.expect(NullPointerException.class); - expectedException.expectMessage("qualityGate can't be null"); - - builder.setQualityGate(null); - } - - @Test - public void setStatus_fails_with_NPE_if_argument_is_null() { - expectedException.expect(NullPointerException.class); - expectedException.expectMessage("status can't be null"); - - builder.setStatus(null); - } - - @Test - public void build_fails_with_NPE_if_qualityGate_not_set() { - expectedException.expect(NullPointerException.class); - expectedException.expectMessage("qualityGate can't be null"); - - builder.build(); - } - @Test public void build_fails_with_NPE_if_status_not_set() { builder.setQualityGate(NO_CONDITION_QUALITY_GATE); @@ -185,7 +160,7 @@ public class EvaluatedQualityGateTest { public void equals_is_based_on_all_fields() { EvaluatedQualityGate.Builder builder = this.builder .setQualityGate(ONE_CONDITION_QUALITY_GATE) - .setStatus(WARN) + .setStatus(Level.WARN) .addCondition(CONDITION_1, EvaluatedCondition.EvaluationStatus.WARN, "foo"); EvaluatedQualityGate underTest = builder.build(); @@ -194,10 +169,10 @@ public class EvaluatedQualityGateTest { assertThat(underTest).isNotEqualTo(null); assertThat(underTest).isNotEqualTo(new Object()); assertThat(underTest).isNotEqualTo(builder.setQualityGate(new QualityGate("other_id", QUALITY_GATE_NAME, singleton(CONDITION_1))).build()); - assertThat(underTest).isNotEqualTo(builder.setQualityGate(ONE_CONDITION_QUALITY_GATE).setStatus(OK).build()); + assertThat(underTest).isNotEqualTo(builder.setQualityGate(ONE_CONDITION_QUALITY_GATE).setStatus(Level.OK).build()); assertThat(underTest).isNotEqualTo(newBuilder() .setQualityGate(ONE_CONDITION_QUALITY_GATE) - .setStatus(WARN) + .setStatus(Level.WARN) .addCondition(CONDITION_1, EvaluatedCondition.EvaluationStatus.OK, "foo") .build()); } @@ -206,7 +181,7 @@ public class EvaluatedQualityGateTest { public void hashcode_is_based_on_all_fields() { EvaluatedQualityGate.Builder builder = this.builder .setQualityGate(ONE_CONDITION_QUALITY_GATE) - .setStatus(WARN) + .setStatus(Level.WARN) .addCondition(CONDITION_1, EvaluatedCondition.EvaluationStatus.WARN, "foo"); EvaluatedQualityGate underTest = builder.build(); @@ -215,10 +190,10 @@ public class EvaluatedQualityGateTest { assertThat(underTest.hashCode()).isNotEqualTo(null); assertThat(underTest.hashCode()).isNotEqualTo(new Object().hashCode()); assertThat(underTest.hashCode()).isNotEqualTo(builder.setQualityGate(new QualityGate("other_id", QUALITY_GATE_NAME, singleton(CONDITION_1))).build().hashCode()); - assertThat(underTest.hashCode()).isNotEqualTo(builder.setQualityGate(ONE_CONDITION_QUALITY_GATE).setStatus(OK).build().hashCode()); + assertThat(underTest.hashCode()).isNotEqualTo(builder.setQualityGate(ONE_CONDITION_QUALITY_GATE).setStatus(Level.OK).build().hashCode()); assertThat(underTest.hashCode()).isNotEqualTo(newBuilder() .setQualityGate(ONE_CONDITION_QUALITY_GATE) - .setStatus(WARN) + .setStatus(Level.WARN) .addCondition(CONDITION_1, EvaluatedCondition.EvaluationStatus.OK, "foo") .build().hashCode()); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualitygate/LiveQualityGateFactoryImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/LiveQualityGateFactoryImplTest.java deleted file mode 100644 index 955d56ba35e..00000000000 --- a/server/sonar-server/src/test/java/org/sonar/server/qualitygate/LiveQualityGateFactoryImplTest.java +++ /dev/null @@ -1,186 +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 com.google.common.collect.ImmutableList; -import java.util.List; -import java.util.Optional; -import java.util.Random; -import java.util.stream.IntStream; -import javax.annotation.Nullable; -import org.assertj.core.groups.Tuple; -import org.junit.Rule; -import org.junit.Test; -import org.sonar.api.config.internal.MapSettings; -import org.sonar.api.issue.Issue; -import org.sonar.api.rules.RuleType; -import org.sonar.api.utils.System2; -import org.sonar.core.util.stream.MoreCollectors; -import org.sonar.db.DbTester; -import org.sonar.db.component.BranchDto; -import org.sonar.db.component.BranchType; -import org.sonar.db.component.ComponentDto; -import org.sonar.db.organization.OrganizationDto; -import org.sonar.db.rule.RuleDefinitionDto; -import org.sonar.db.rule.RuleTesting; -import org.sonar.server.es.EsTester; -import org.sonar.server.issue.index.IssueIndex; -import org.sonar.server.issue.index.IssueIndexDefinition; -import org.sonar.server.issue.index.IssueIndexer; -import org.sonar.server.issue.index.IssueIteratorFactory; -import org.sonar.server.permission.index.AuthorizationTypeSupport; -import org.sonar.server.tester.UserSessionRule; - -import static java.lang.String.valueOf; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.groups.Tuple.tuple; -import static org.sonar.api.measures.CoreMetrics.BUGS_KEY; -import static org.sonar.api.measures.CoreMetrics.CODE_SMELLS_KEY; -import static org.sonar.api.measures.CoreMetrics.VULNERABILITIES_KEY; -import static org.sonar.db.component.ComponentTesting.newBranchDto; - -public class LiveQualityGateFactoryImplTest { - private static final List OPEN_STATUSES = ImmutableList.of(Issue.STATUS_OPEN, Issue.STATUS_CONFIRMED); - private static final List NON_OPEN_STATUSES = Issue.STATUSES.stream().filter(OPEN_STATUSES::contains).collect(MoreCollectors.toList()); - - @Rule - public DbTester dbTester = DbTester.create(System2.INSTANCE); - @Rule - public EsTester esTester = new EsTester(new IssueIndexDefinition(new MapSettings().asConfig())); - @Rule - public UserSessionRule userSessionRule = UserSessionRule.standalone(); - - private Random random = new Random(); - private String randomOpenStatus = OPEN_STATUSES.get(random.nextInt(OPEN_STATUSES.size())); - private String randomNonOpenStatus = NON_OPEN_STATUSES.get(random.nextInt(NON_OPEN_STATUSES.size())); - private String randomResolution = Issue.RESOLUTIONS.get(random.nextInt(Issue.RESOLUTIONS.size())); - private IssueIndexer issueIndexer = new IssueIndexer(esTester.client(), dbTester.getDbClient(), new IssueIteratorFactory(dbTester.getDbClient())); - private IssueIndex issueIndex = new IssueIndex(esTester.client(), System2.INSTANCE, userSessionRule, new AuthorizationTypeSupport(userSessionRule)); - - private LiveQualityGateFactoryImpl underTest = new LiveQualityGateFactoryImpl(issueIndex, System2.INSTANCE); - - @Test - public void compute_QG_ok_if_there_is_no_issue_in_index_ignoring_permissions() { - OrganizationDto organization = dbTester.organizations().insert(); - ComponentDto project = insertPrivateBranch(organization, BranchType.SHORT); - - EvaluatedQualityGate qualityGate = underTest.buildForShortLivedBranch(project); - - assertThat(qualityGate.getStatus()).isEqualTo(EvaluatedQualityGate.Status.OK); - assertThat(qualityGate.getEvaluatedConditions()) - .extracting(EvaluatedCondition::getStatus, EvaluatedCondition::getValue) - .containsOnly(tuple(EvaluatedCondition.EvaluationStatus.OK, Optional.of("0"))); - } - - @Test - public void computes_QG_error_if_there_is_one_unresolved_bug_issue_in_index_ignoring_permissions() { - int unresolvedIssues = 1 + random.nextInt(10); - - computesQGErrorIfThereIsAtLeastOneUnresolvedIssueInIndexOfTypeIgnoringPermissions( - unresolvedIssues, - RuleType.BUG, - tuple(BUGS_KEY, EvaluatedCondition.EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedIssues))), - tuple(VULNERABILITIES_KEY, EvaluatedCondition.EvaluationStatus.OK, Optional.of("0")), - tuple(CODE_SMELLS_KEY, EvaluatedCondition.EvaluationStatus.OK, Optional.of("0"))); - } - - @Test - public void computes_QG_error_if_there_is_one_unresolved_vulnerability_issue_in_index_ignoring_permissions() { - int unresolvedIssues = 1 + random.nextInt(10); - - computesQGErrorIfThereIsAtLeastOneUnresolvedIssueInIndexOfTypeIgnoringPermissions( - unresolvedIssues, - RuleType.VULNERABILITY, - tuple(BUGS_KEY, EvaluatedCondition.EvaluationStatus.OK, Optional.of("0")), - tuple(VULNERABILITIES_KEY, EvaluatedCondition.EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedIssues))), - tuple(CODE_SMELLS_KEY, EvaluatedCondition.EvaluationStatus.OK, Optional.of("0"))); - } - - @Test - public void computes_QG_error_if_there_is_one_unresolved_codeSmell_issue_in_index_ignoring_permissions() { - int unresolvedIssues = 1 + random.nextInt(10); - - computesQGErrorIfThereIsAtLeastOneUnresolvedIssueInIndexOfTypeIgnoringPermissions( - unresolvedIssues, - RuleType.CODE_SMELL, - tuple(BUGS_KEY, EvaluatedCondition.EvaluationStatus.OK, Optional.of("0")), - tuple(VULNERABILITIES_KEY, EvaluatedCondition.EvaluationStatus.OK, Optional.of("0")), - tuple(CODE_SMELLS_KEY, EvaluatedCondition.EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedIssues)))); - } - - private void computesQGErrorIfThereIsAtLeastOneUnresolvedIssueInIndexOfTypeIgnoringPermissions( - int unresolvedIssues, RuleType ruleType, Tuple... expectedQGConditions) { - OrganizationDto organization = dbTester.organizations().insert(); - ComponentDto project = insertPrivateBranch(organization, BranchType.SHORT); - IntStream.range(0, unresolvedIssues).forEach(i -> insertIssue(project, ruleType, randomOpenStatus, null)); - IntStream.range(0, random.nextInt(10)).forEach(i -> insertIssue(project, ruleType, randomNonOpenStatus, randomResolution)); - indexIssues(project); - - EvaluatedQualityGate qualityGate = underTest.buildForShortLivedBranch(project); - - assertThat(qualityGate.getStatus()).isEqualTo(EvaluatedQualityGate.Status.ERROR); - assertThat(qualityGate.getEvaluatedConditions()) - .extracting(s -> s.getCondition().getMetricKey(), EvaluatedCondition::getStatus, EvaluatedCondition::getValue) - .containsOnly(expectedQGConditions); - } - - @Test - public void computes_QG_error_with_all_failing_conditions() { - OrganizationDto organization = dbTester.organizations().insert(); - ComponentDto project = insertPrivateBranch(organization, BranchType.SHORT); - int unresolvedBugs = 1 + random.nextInt(10); - int unresolvedVulnerabilities = 1 + random.nextInt(10); - int unresolvedCodeSmells = 1 + random.nextInt(10); - IntStream.range(0, unresolvedBugs).forEach(i -> insertIssue(project, RuleType.BUG, randomOpenStatus, null)); - IntStream.range(0, unresolvedVulnerabilities).forEach(i -> insertIssue(project, RuleType.VULNERABILITY, randomOpenStatus, null)); - IntStream.range(0, unresolvedCodeSmells).forEach(i -> insertIssue(project, RuleType.CODE_SMELL, randomOpenStatus, null)); - indexIssues(project); - - EvaluatedQualityGate qualityGate = underTest.buildForShortLivedBranch(project); - - assertThat(qualityGate.getStatus()).isEqualTo(EvaluatedQualityGate.Status.ERROR); - assertThat(qualityGate.getEvaluatedConditions()) - .extracting(s -> s.getCondition().getMetricKey(), EvaluatedCondition::getStatus, EvaluatedCondition::getValue) - .containsOnly( - Tuple.tuple(BUGS_KEY, EvaluatedCondition.EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedBugs))), - Tuple.tuple(VULNERABILITIES_KEY, EvaluatedCondition.EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedVulnerabilities))), - Tuple.tuple(CODE_SMELLS_KEY, EvaluatedCondition.EvaluationStatus.ERROR, Optional.of(valueOf(unresolvedCodeSmells)))); - } - - private void indexIssues(ComponentDto project) { - issueIndexer.indexOnAnalysis(project.uuid()); - } - - private void insertIssue(ComponentDto component, RuleType ruleType, String status, @Nullable String resolution) { - RuleDefinitionDto rule = RuleTesting.newRule(); - dbTester.rules().insert(rule); - dbTester.commit(); - dbTester.issues().insert(rule, component, component, i -> i.setType(ruleType).setStatus(status).setResolution(resolution)); - dbTester.commit(); - } - - private ComponentDto insertPrivateBranch(OrganizationDto organization, BranchType branchType) { - ComponentDto project = dbTester.components().insertPrivateProject(organization); - BranchDto branchDto = newBranchDto(project.projectUuid(), branchType) - .setKey("foo"); - return dbTester.components().insertProjectBranch(project, branchDto); - } - -} diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/LiveQualityGateFactory.java b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/QualityGateConverterTest.java similarity index 84% rename from server/sonar-server/src/main/java/org/sonar/server/qualitygate/LiveQualityGateFactory.java rename to server/sonar-server/src/test/java/org/sonar/server/qualitygate/QualityGateConverterTest.java index c0638c88a5d..c00312694b3 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/LiveQualityGateFactory.java +++ b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/QualityGateConverterTest.java @@ -19,8 +19,13 @@ */ package org.sonar.server.qualitygate; -import org.sonar.db.component.ComponentDto; +import org.junit.Test; + +public class QualityGateConverterTest { + + @Test + public void test_ToJson() { + // FIXME + } -public interface LiveQualityGateFactory { - EvaluatedQualityGate buildForShortLivedBranch(ComponentDto componentDto); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualitygate/QualityGateFinderTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/QualityGateFinderTest.java index b037cedc155..fc05daf97d4 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/qualitygate/QualityGateFinderTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/QualityGateFinderTest.java @@ -49,7 +49,7 @@ public class QualityGateFinderTest { ComponentDto project = db.components().insertPrivateProject(); QualityGateDto dbQualityGate = db.qualityGates().createDefaultQualityGate(db.getDefaultOrganization(), qg -> qg.setName("Sonar way")); - QualityGateFinder.QualityGateData result = underTest.getQualityGate(dbSession, db.getDefaultOrganization(), project.getId()); + QualityGateFinder.QualityGateData result = underTest.getQualityGate(dbSession, db.getDefaultOrganization(), project); assertThat(result).isNotNull(); assertThat(result.getQualityGate().getId()).isEqualTo(dbQualityGate.getId()); @@ -63,7 +63,7 @@ public class QualityGateFinderTest { QualityGateDto dbQualityGate = db.qualityGates().insertQualityGate(db.getDefaultOrganization(), qg -> qg.setName("My team QG")); db.qualityGates().associateProjectToQualityGate(project, dbQualityGate); - QualityGateFinder.QualityGateData result = underTest.getQualityGate(dbSession, db.getDefaultOrganization(), project.getId()); + QualityGateFinder.QualityGateData result = underTest.getQualityGate(dbSession, db.getDefaultOrganization(), project); assertThat(result).isNotNull(); assertThat(result.getQualityGate().getId()).isEqualTo(dbQualityGate.getId()); @@ -80,7 +80,7 @@ public class QualityGateFinderTest { expectedException.expect(IllegalStateException.class); expectedException.expectMessage(format("Unable to find the quality gate [%s] for organization [%s]", dbQualityGate.getUuid(), db.getDefaultOrganization().getUuid())); - underTest.getQualityGate(dbSession, db.getDefaultOrganization(), project.getId()); + underTest.getQualityGate(dbSession, db.getDefaultOrganization(), project); } @Test @@ -91,7 +91,7 @@ public class QualityGateFinderTest { db.getDbClient().qualityGateDao().delete(dbQualityGate, dbSession); expectedException.expect(NotFoundException.class); - underTest.getQualityGate(dbSession, db.getDefaultOrganization(), project.getId()); + underTest.getQualityGate(dbSession, db.getDefaultOrganization(), project); } @Test diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualitygate/QualityGateModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/QualityGateModuleTest.java index 686a0a543f1..d0dd0201970 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/qualitygate/QualityGateModuleTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/QualityGateModuleTest.java @@ -29,6 +29,6 @@ public class QualityGateModuleTest { public void verify_count_of_added_components() { ComponentContainer container = new ComponentContainer(); new QualityGateModule().configure(container); - assertThat(container.size()).isEqualTo(20 + 2); + assertThat(container.size()).isEqualTo(21 + 2); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventFactoryImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventFactoryImplTest.java deleted file mode 100644 index ae01892df4f..00000000000 --- a/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventFactoryImplTest.java +++ /dev/null @@ -1,594 +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 com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import com.tngtech.java.junit.dataprovider.DataProvider; -import com.tngtech.java.junit.dataprovider.DataProviderRunner; -import com.tngtech.java.junit.dataprovider.UseDataProvider; -import java.util.Arrays; -import java.util.Collection; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.Stream; -import javax.annotation.Nullable; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Matchers; -import org.mockito.Mockito; -import org.sonar.api.config.Configuration; -import org.sonar.api.issue.DefaultTransitions; -import org.sonar.api.issue.Issue; -import org.sonar.api.rules.RuleType; -import org.sonar.api.utils.System2; -import org.sonar.core.issue.IssueChangeContext; -import org.sonar.core.util.UuidFactoryFast; -import org.sonar.core.util.stream.MoreCollectors; -import org.sonar.db.DbClient; -import org.sonar.db.DbSession; -import org.sonar.db.DbTester; -import org.sonar.db.component.AnalysisPropertyDto; -import org.sonar.db.component.BranchDao; -import org.sonar.db.component.BranchDto; -import org.sonar.db.component.BranchType; -import org.sonar.db.component.ComponentDao; -import org.sonar.db.component.ComponentDto; -import org.sonar.db.component.ComponentTesting; -import org.sonar.db.component.SnapshotDao; -import org.sonar.db.component.SnapshotDto; -import org.sonar.db.issue.IssueDto; -import org.sonar.db.organization.OrganizationDto; -import org.sonar.server.qualitygate.LiveQualityGateFactory; -import org.sonar.server.qualitygate.changeevent.QGChangeEventFactory.IssueChange; -import org.sonar.server.settings.ProjectConfigurationLoader; -import org.sonar.server.tester.UserSessionRule; -import org.sonar.server.webhook.WebhookPayloadFactory; - -import static com.google.common.base.Preconditions.checkArgument; -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.apache.commons.lang.RandomStringUtils.randomAlphanumeric; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.tuple; -import static org.mockito.Mockito.mock; -import static org.sonar.core.util.stream.MoreCollectors.toArrayList; -import static org.sonar.db.component.ComponentTesting.newBranchDto; - -@RunWith(DataProviderRunner.class) -public class QGChangeEventFactoryImplTest { - @Rule - public DbTester dbTester = DbTester.create(System2.INSTANCE); - @Rule - public UserSessionRule userSessionRule = UserSessionRule.standalone(); - - private DbClient dbClient = dbTester.getDbClient(); - - private Random random = new Random(); - private RuleType randomRuleType = RuleType.values()[random.nextInt(RuleType.values().length)]; - - private IssueChangeContext scanChangeContext = IssueChangeContext.createScan(new Date()); - private IssueChangeContext userChangeContext = IssueChangeContext.createUser(new Date(), "userLogin"); - private WebhookPayloadFactory webhookPayloadFactory = mock(WebhookPayloadFactory.class); - private DbClient spiedOnDbClient = Mockito.spy(dbClient); - private ProjectConfigurationLoader projectConfigurationLoader = mock(ProjectConfigurationLoader.class); - private LiveQualityGateFactory liveQualityGateFactory = mock(LiveQualityGateFactory.class); - private QGChangeEventFactoryImpl underTest = new QGChangeEventFactoryImpl(spiedOnDbClient, projectConfigurationLoader, liveQualityGateFactory); - private DbClient mockedDbClient = mock(DbClient.class); - private QGChangeEventFactoryImpl mockedUnderTest = new QGChangeEventFactoryImpl(mockedDbClient, projectConfigurationLoader, liveQualityGateFactory); - - @Test - public void on_type_change_has_no_effect_if_SearchResponseData_has_no_issue() { - mockedUnderTest.from(issueChangeData(), new IssueChange(randomRuleType), userChangeContext); - - Mockito.verifyZeroInteractions(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory); - } - - @Test - public void on_type_change_has_no_effect_if_scan_IssueChangeContext() { - mockedUnderTest.from(issueChangeData(), new IssueChange(randomRuleType), scanChangeContext); - - Mockito.verifyZeroInteractions(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory); - } - - @Test - public void on_transition_change_has_no_effect_if_SearchResponseData_has_no_issue() { - mockedUnderTest.from(issueChangeData(), new IssueChange(randomAlphanumeric(12)), userChangeContext); - - Mockito.verifyZeroInteractions(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory); - } - - @Test - public void onTransition_has_no_effect_if_transition_key_is_empty() { - on_transition_changeHasNoEffectForTransitionKey(""); - } - - @Test - public void onTransition_has_no_effect_if_transition_key_is_random() { - on_transition_changeHasNoEffectForTransitionKey(randomAlphanumeric(99)); - } - - @Test - public void on_transition_change_has_no_effect_if_transition_key_is_ignored_default_transition_key() { - Set supportedDefaultTransitionKeys = ImmutableSet.of( - DefaultTransitions.RESOLVE, DefaultTransitions.FALSE_POSITIVE, DefaultTransitions.WONT_FIX, DefaultTransitions.REOPEN); - DefaultTransitions.ALL.stream() - .filter(s -> !supportedDefaultTransitionKeys.contains(s)) - .forEach(this::on_transition_changeHasNoEffectForTransitionKey); - } - - private void on_transition_changeHasNoEffectForTransitionKey(@Nullable String transitionKey) { - Mockito.reset(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory); - - mockedUnderTest.from(issueChangeData(newIssueDto()), new IssueChange(transitionKey), userChangeContext); - - Mockito.verifyZeroInteractions(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory); - } - - @Test - public void on_transition_change_has_no_effect_if_scan_IssueChangeContext() { - mockedUnderTest.from(issueChangeData(newIssueDto()), new IssueChange(randomAlphanumeric(12)), scanChangeContext); - - Mockito.verifyZeroInteractions(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory); - } - - @Test - public void on_type_and_transition_change_has_no_effect_if_SearchResponseData_has_no_issue() { - mockedUnderTest.from(issueChangeData(), new IssueChange(randomRuleType, randomAlphanumeric(3)), userChangeContext); - - Mockito.verifyZeroInteractions(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory); - } - - @Test - public void on_type_and_transition_change_has_no_effect_if_scan_IssueChangeContext() { - mockedUnderTest.from(issueChangeData(), new IssueChange(randomRuleType, randomAlphanumeric(3)), scanChangeContext); - - Mockito.verifyZeroInteractions(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory); - } - - @Test - public void on_type_and_transition_has_no_effect_if_transition_key_is_empty() { - on_type_and_transition_changeHasNoEffectForTransitionKey(""); - } - - @Test - public void on_type_and_transition_has_no_effect_if_transition_key_is_random() { - on_type_and_transition_changeHasNoEffectForTransitionKey(randomAlphanumeric(66)); - } - - @Test - public void on_type_and_transition_has_no_effect_if_transition_key_is_ignored_default_transition_key() { - Set supportedDefaultTransitionKeys = ImmutableSet.of( - DefaultTransitions.RESOLVE, DefaultTransitions.FALSE_POSITIVE, DefaultTransitions.WONT_FIX, DefaultTransitions.REOPEN); - DefaultTransitions.ALL.stream() - .filter(s -> !supportedDefaultTransitionKeys.contains(s)) - .forEach(this::on_type_and_transition_changeHasNoEffectForTransitionKey); - } - - private void on_type_and_transition_changeHasNoEffectForTransitionKey(@Nullable String transitionKey) { - Mockito.reset(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory); - - mockedUnderTest.from(issueChangeData(newIssueDto()), new IssueChange(randomRuleType, transitionKey), userChangeContext); - - Mockito.verifyZeroInteractions(mockedDbClient, projectConfigurationLoader, webhookPayloadFactory); - } - - @Test - @UseDataProvider("validIssueChanges") - public void broadcast_to_QGEventListeners_for_short_living_branch_of_issue(IssueChange issueChange) { - OrganizationDto organization = dbTester.organizations().insert(); - ComponentDto project = dbTester.components().insertPublicProject(organization); - ComponentAndBranch branch = insertProjectBranch(project, BranchType.SHORT, "foo"); - SnapshotDto analysis = insertAnalysisTask(branch); - Configuration configuration = mockLoadProjectConfiguration(branch); - - Map properties = new HashMap<>(); - properties.put("sonar.analysis.test1", randomAlphanumeric(50)); - properties.put("sonar.analysis.test2", randomAlphanumeric(5000)); - insertPropertiesFor(analysis.getUuid(), properties); - - Collection events = underTest.from(issueChangeData(newIssueDto(branch)), issueChange, userChangeContext); - - assertThat(events).hasSize(1); - QGChangeEvent event = events.iterator().next(); - assertThat(event.getProject()).isEqualTo(branch.component); - assertThat(event.getBranch()).isEqualTo(branch.branch); - assertThat(event.getAnalysis()).isEqualTo(analysis); - assertThat(event.getProjectConfiguration()).isSameAs(configuration); - } - - @Test - public void do_not_load_project_configuration_nor_analysis_nor_call_webhook_if_there_are_no_short_branch() { - OrganizationDto organization = dbTester.organizations().insert(); - ComponentDto project = dbTester.components().insertPublicProject(organization); - ComponentAndBranch longBranch1 = insertProjectBranch(project, BranchType.LONG, "foo"); - ComponentAndBranch longBranch2 = insertProjectBranch(project, BranchType.LONG, "bar"); - ImmutableList issueDtos = ImmutableList.of(newIssueDto(project), newIssueDto(longBranch1), newIssueDto(longBranch2)); - - SnapshotDao snapshotDaoSpy = Mockito.spy(dbClient.snapshotDao()); - Mockito.when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy); - underTest.from(issueChangeData(issueDtos), new IssueChange(randomRuleType), userChangeContext); - - Mockito.verifyZeroInteractions(projectConfigurationLoader); - Mockito.verify(snapshotDaoSpy, Mockito.times(0)).selectLastAnalysesByRootComponentUuids(Matchers.any(DbSession.class), Matchers.anyCollectionOf(String.class)); - } - - @Test - public void creates_single_QGChangeEvent_per_short_branch_with_at_least_one_issue_in_SearchResponseData() { - OrganizationDto organization = dbTester.organizations().insert(); - ComponentAndBranch branch1 = insertPrivateBranch(organization, BranchType.SHORT); - ComponentAndBranch branch2 = insertPrivateBranch(organization, BranchType.SHORT); - ComponentAndBranch branch3 = insertPrivateBranch(organization, BranchType.SHORT); - SnapshotDto analysis1 = insertAnalysisTask(branch1); - SnapshotDto analysis2 = insertAnalysisTask(branch2); - SnapshotDto analysis3 = insertAnalysisTask(branch3); - int issuesBranch1 = 2 + random.nextInt(10); - int issuesBranch2 = 2 + random.nextInt(10); - int issuesBranch3 = 2 + random.nextInt(10); - List issueDtos = Stream.of( - IntStream.range(0, issuesBranch1).mapToObj(i -> newIssueDto(branch1.component)), - IntStream.range(0, issuesBranch2).mapToObj(i -> newIssueDto(branch2.component)), - IntStream.range(0, issuesBranch3).mapToObj(i -> newIssueDto(branch3.component))) - .flatMap(s -> s) - .collect(MoreCollectors.toList()); - Configuration configuration1 = mock(Configuration.class); - Configuration configuration2 = mock(Configuration.class); - Configuration configuration3 = mock(Configuration.class); - mockLoadProjectConfigurations( - branch1.component, configuration1, - branch2.component, configuration2, - branch3.component, configuration3); - - ComponentDao componentDaoSpy = Mockito.spy(dbClient.componentDao()); - BranchDao branchDaoSpy = Mockito.spy(dbClient.branchDao()); - SnapshotDao snapshotDaoSpy = Mockito.spy(dbClient.snapshotDao()); - Mockito.when(spiedOnDbClient.componentDao()).thenReturn(componentDaoSpy); - Mockito.when(spiedOnDbClient.branchDao()).thenReturn(branchDaoSpy); - Mockito.when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy); - Collection qgChangeEvents = underTest.from(issueChangeData(issueDtos), - new IssueChange(randomRuleType), userChangeContext); - - assertThat(qgChangeEvents) - .hasSize(3) - .extracting(QGChangeEvent::getBranch, QGChangeEvent::getProjectConfiguration, QGChangeEvent::getAnalysis) - .containsOnly( - tuple(branch1.branch, configuration1, analysis1), - tuple(branch2.branch, configuration2, analysis2), - tuple(branch3.branch, configuration3, analysis3)); - - // verifyWebhookCalled(branch1, configuration1, analysis1); - // verifyWebhookCalled(branch2, configuration2, analysis2); - // verifyWebhookCalled(branch3, configuration3, analysis3); - // extractPayloadFactoryArguments(3); - - Set uuids = ImmutableSet.of(branch1.uuid(), branch2.uuid(), branch3.uuid()); - Mockito.verify(componentDaoSpy).selectByUuids(Matchers.any(DbSession.class), Matchers.eq(uuids)); - Mockito.verify(branchDaoSpy).selectByUuids(Matchers.any(DbSession.class), Matchers.eq(uuids)); - Mockito.verify(snapshotDaoSpy).selectLastAnalysesByRootComponentUuids(Matchers.any(DbSession.class), Matchers.eq(uuids)); - Mockito.verifyNoMoreInteractions(componentDaoSpy, branchDaoSpy, snapshotDaoSpy); - } - - @Test - public void create_QGChangeEvent_only_for_short_branches() { - OrganizationDto organization = dbTester.organizations().insert(); - ComponentAndBranch shortBranch = insertPrivateBranch(organization, BranchType.SHORT); - ComponentAndBranch longBranch = insertPrivateBranch(organization, BranchType.LONG); - SnapshotDto analysis1 = insertAnalysisTask(shortBranch); - SnapshotDto analysis2 = insertAnalysisTask(longBranch); - Configuration configuration = mockLoadProjectConfiguration(shortBranch); - - ComponentDao componentDaoSpy = Mockito.spy(dbClient.componentDao()); - BranchDao branchDaoSpy = Mockito.spy(dbClient.branchDao()); - SnapshotDao snapshotDaoSpy = Mockito.spy(dbClient.snapshotDao()); - Mockito.when(spiedOnDbClient.componentDao()).thenReturn(componentDaoSpy); - Mockito.when(spiedOnDbClient.branchDao()).thenReturn(branchDaoSpy); - Mockito.when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy); - Collection qgChangeEvents = underTest.from( - issueChangeData(asList(newIssueDto(shortBranch), newIssueDto(longBranch))), - new IssueChange(randomRuleType), - userChangeContext); - - assertThat(qgChangeEvents) - .hasSize(1) - .extracting(QGChangeEvent::getBranch, QGChangeEvent::getProjectConfiguration, QGChangeEvent::getAnalysis) - .containsOnly(tuple(shortBranch.branch, configuration, analysis1)); - - Set uuids = ImmutableSet.of(shortBranch.uuid(), longBranch.uuid()); - Mockito.verify(componentDaoSpy).selectByUuids(Matchers.any(DbSession.class), Matchers.eq(uuids)); - Mockito.verify(branchDaoSpy).selectByUuids(Matchers.any(DbSession.class), Matchers.eq(uuids)); - Mockito.verify(snapshotDaoSpy).selectLastAnalysesByRootComponentUuids(Matchers.any(DbSession.class), Matchers.eq(ImmutableSet.of(shortBranch.uuid()))); - Mockito.verifyNoMoreInteractions(componentDaoSpy, branchDaoSpy, snapshotDaoSpy); - } - - @Test - public void do_not_load_componentDto_from_DB_if_all_are_in_inputData() { - OrganizationDto organization = dbTester.organizations().insert(); - ComponentAndBranch branch1 = insertPrivateBranch(organization, BranchType.SHORT); - ComponentAndBranch branch2 = insertPrivateBranch(organization, BranchType.SHORT); - ComponentAndBranch branch3 = insertPrivateBranch(organization, BranchType.SHORT); - SnapshotDto analysis1 = insertAnalysisTask(branch1); - SnapshotDto analysis2 = insertAnalysisTask(branch2); - SnapshotDto analysis3 = insertAnalysisTask(branch3); - List issueDtos = asList(newIssueDto(branch1), newIssueDto(branch2), newIssueDto(branch3)); - Configuration configuration1 = mock(Configuration.class); - Configuration configuration2 = mock(Configuration.class); - Configuration configuration3 = mock(Configuration.class); - mockLoadProjectConfigurations( - branch1.component, configuration1, - branch2.component, configuration2, - branch3.component, configuration3); - - ComponentDao componentDaoSpy = Mockito.spy(dbClient.componentDao()); - Mockito.when(spiedOnDbClient.componentDao()).thenReturn(componentDaoSpy); - Collection qgChangeEvents = underTest.from( - issueChangeData(issueDtos, branch1, branch2, branch3), - new IssueChange(randomRuleType), - userChangeContext); - - assertThat(qgChangeEvents) - .hasSize(3) - .extracting(QGChangeEvent::getBranch, QGChangeEvent::getProjectConfiguration, QGChangeEvent::getAnalysis) - .containsOnly( - tuple(branch1.branch, configuration1, analysis1), - tuple(branch2.branch, configuration2, analysis2), - tuple(branch3.branch, configuration3, analysis3)); - - Mockito.verify(componentDaoSpy, Mockito.times(0)).selectByUuids(Matchers.any(DbSession.class), Matchers.anyCollectionOf(String.class)); - Mockito.verifyNoMoreInteractions(componentDaoSpy); - } - - @Test - public void call_db_only_for_componentDto_not_in_inputData() { - OrganizationDto organization = dbTester.organizations().insert(); - ComponentAndBranch branch1 = insertPrivateBranch(organization, BranchType.SHORT); - ComponentAndBranch branch2 = insertPrivateBranch(organization, BranchType.SHORT); - ComponentAndBranch branch3 = insertPrivateBranch(organization, BranchType.SHORT); - SnapshotDto analysis1 = insertAnalysisTask(branch1); - SnapshotDto analysis2 = insertAnalysisTask(branch2); - SnapshotDto analysis3 = insertAnalysisTask(branch3); - List issueDtos = asList(newIssueDto(branch1), newIssueDto(branch2), newIssueDto(branch3)); - Configuration configuration1 = mock(Configuration.class); - Configuration configuration2 = mock(Configuration.class); - Configuration configuration3 = mock(Configuration.class); - mockLoadProjectConfigurations( - branch1.component, configuration1, - branch2.component, configuration2, - branch3.component, configuration3); - - ComponentDao componentDaoSpy = Mockito.spy(dbClient.componentDao()); - BranchDao branchDaoSpy = Mockito.spy(dbClient.branchDao()); - SnapshotDao snapshotDaoSpy = Mockito.spy(dbClient.snapshotDao()); - Mockito.when(spiedOnDbClient.componentDao()).thenReturn(componentDaoSpy); - Mockito.when(spiedOnDbClient.branchDao()).thenReturn(branchDaoSpy); - Mockito.when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy); - Collection qgChangeEvents = underTest.from( - issueChangeData(issueDtos, branch1, branch3), - new IssueChange(randomRuleType), - userChangeContext); - - assertThat(qgChangeEvents).hasSize(3); - - Set uuids = ImmutableSet.of(branch1.uuid(), branch2.uuid(), branch3.uuid()); - Mockito.verify(componentDaoSpy).selectByUuids(Matchers.any(DbSession.class), Matchers.eq(ImmutableSet.of(branch2.uuid()))); - Mockito.verify(branchDaoSpy).selectByUuids(Matchers.any(DbSession.class), Matchers.eq(uuids)); - Mockito.verify(snapshotDaoSpy).selectLastAnalysesByRootComponentUuids(Matchers.any(DbSession.class), Matchers.eq(uuids)); - Mockito.verifyNoMoreInteractions(componentDaoSpy, branchDaoSpy, snapshotDaoSpy); - } - - @Test - public void supports_issues_on_files_and_filter_on_short_branches_asap_when_calling_db() { - OrganizationDto organization = dbTester.organizations().insert(); - ComponentDto project = dbTester.components().insertPrivateProject(organization); - ComponentDto file = dbTester.components().insertComponent(ComponentTesting.newFileDto(project)); - ComponentAndBranch shortBranch = insertProjectBranch(project, BranchType.SHORT, "foo"); - ComponentAndBranch longBranch = insertProjectBranch(project, BranchType.LONG, "bar"); - ComponentDto shortBranchFile = dbTester.components().insertComponent(ComponentTesting.newFileDto(shortBranch.component)); - ComponentDto longBranchFile = dbTester.components().insertComponent(ComponentTesting.newFileDto(longBranch.component)); - SnapshotDto analysis1 = insertAnalysisTask(project); - SnapshotDto analysis2 = insertAnalysisTask(shortBranch); - SnapshotDto analysis3 = insertAnalysisTask(longBranch); - List issueDtos = asList( - newIssueDto(file, project), - newIssueDto(shortBranchFile, shortBranch), - newIssueDto(longBranchFile, longBranch)); - Configuration configuration = mockLoadProjectConfiguration(shortBranch); - - ComponentDao componentDaoSpy = Mockito.spy(dbClient.componentDao()); - BranchDao branchDaoSpy = Mockito.spy(dbClient.branchDao()); - SnapshotDao snapshotDaoSpy = Mockito.spy(dbClient.snapshotDao()); - Mockito.when(spiedOnDbClient.componentDao()).thenReturn(componentDaoSpy); - Mockito.when(spiedOnDbClient.branchDao()).thenReturn(branchDaoSpy); - Mockito.when(spiedOnDbClient.snapshotDao()).thenReturn(snapshotDaoSpy); - Collection qgChangeEvents = underTest.from(issueChangeData(issueDtos), - new IssueChange(randomRuleType), userChangeContext); - - assertThat(qgChangeEvents) - .hasSize(1) - .extracting(QGChangeEvent::getBranch, QGChangeEvent::getProjectConfiguration, QGChangeEvent::getAnalysis) - .containsOnly(tuple(shortBranch.branch, configuration, analysis2)); - - Set uuids = ImmutableSet.of(project.uuid(), shortBranch.uuid(), longBranch.uuid()); - Mockito.verify(componentDaoSpy).selectByUuids(Matchers.any(DbSession.class), Matchers.eq(uuids)); - Mockito.verify(branchDaoSpy).selectByUuids(Matchers.any(DbSession.class), Matchers.eq(ImmutableSet.of(shortBranch.uuid(), longBranch.uuid()))); - Mockito.verify(snapshotDaoSpy).selectLastAnalysesByRootComponentUuids(Matchers.any(DbSession.class), Matchers.eq(ImmutableSet.of(shortBranch.uuid()))); - Mockito.verifyNoMoreInteractions(componentDaoSpy, branchDaoSpy, snapshotDaoSpy); - } - - private Configuration mockLoadProjectConfiguration(ComponentAndBranch componentAndBranch) { - Configuration configuration = mock(Configuration.class); - Mockito.when(projectConfigurationLoader.loadProjectConfigurations(Matchers.any(DbSession.class), Matchers.eq(singleton(componentAndBranch.component)))) - .thenReturn(ImmutableMap.of(componentAndBranch.uuid(), configuration)); - return configuration; - } - - private void mockLoadProjectConfigurations(Object... branchesAndConfiguration) { - checkArgument(branchesAndConfiguration.length % 2 == 0); - Set components = new HashSet<>(); - Map result = new HashMap<>(); - for (int i = 0; i < branchesAndConfiguration.length; i++) { - ComponentDto component = (ComponentDto) branchesAndConfiguration[i++]; - Configuration configuration = (Configuration) branchesAndConfiguration[i]; - components.add(component); - result.put(component.uuid(), configuration); - } - Mockito.when(projectConfigurationLoader.loadProjectConfigurations(Matchers.any(DbSession.class), Matchers.eq(components))) - .thenReturn(result); - } - - private ComponentAndBranch insertPrivateBranch(OrganizationDto organization, BranchType branchType) { - ComponentDto project = dbTester.components().insertPrivateProject(organization); - BranchDto branchDto = newBranchDto(project.projectUuid(), branchType) - .setKey("foo"); - ComponentDto newComponent = dbTester.components().insertProjectBranch(project, branchDto); - return new ComponentAndBranch(newComponent, branchDto); - } - - public ComponentAndBranch insertProjectBranch(ComponentDto project, BranchType type, String branchKey) { - BranchDto branchDto = newBranchDto(project.projectUuid(), type).setKey(branchKey); - ComponentDto newComponent = dbTester.components().insertProjectBranch(project, branchDto); - return new ComponentAndBranch(newComponent, branchDto); - } - - private static class ComponentAndBranch { - private final ComponentDto component; - - private final BranchDto branch; - - private ComponentAndBranch(ComponentDto component, BranchDto branch) { - this.component = component; - this.branch = branch; - } - - public ComponentDto getComponent() { - return component; - } - - public BranchDto getBranch() { - return branch; - } - - public String uuid() { - return component.uuid(); - } - - } - - private void insertPropertiesFor(String snapshotUuid, Map properties) { - List analysisProperties = properties.entrySet().stream() - .map(entry -> new AnalysisPropertyDto() - .setUuid(UuidFactoryFast.getInstance().create()) - .setSnapshotUuid(snapshotUuid) - .setKey(entry.getKey()) - .setValue(entry.getValue())) - .collect(toArrayList(properties.size())); - dbTester.getDbClient().analysisPropertiesDao().insert(dbTester.getSession(), analysisProperties); - dbTester.getSession().commit(); - } - - private SnapshotDto insertAnalysisTask(ComponentAndBranch componentAndBranch) { - return insertAnalysisTask(componentAndBranch.component); - } - - private SnapshotDto insertAnalysisTask(ComponentDto component) { - return dbTester.components().insertSnapshot(component); - } - - @DataProvider - public static Object[][] validIssueChanges() { - return new Object[][] { - {new IssueChange(RuleType.BUG)}, - {new IssueChange(RuleType.VULNERABILITY)}, - {new IssueChange(RuleType.CODE_SMELL)}, - {new IssueChange(DefaultTransitions.RESOLVE)}, - {new IssueChange(RuleType.BUG, DefaultTransitions.RESOLVE)}, - {new IssueChange(RuleType.VULNERABILITY, DefaultTransitions.RESOLVE)}, - {new IssueChange(RuleType.CODE_SMELL, DefaultTransitions.RESOLVE)}, - {new IssueChange(DefaultTransitions.FALSE_POSITIVE)}, - {new IssueChange(RuleType.BUG, DefaultTransitions.FALSE_POSITIVE)}, - {new IssueChange(RuleType.VULNERABILITY, DefaultTransitions.FALSE_POSITIVE)}, - {new IssueChange(RuleType.CODE_SMELL, DefaultTransitions.FALSE_POSITIVE)}, - {new IssueChange(DefaultTransitions.WONT_FIX)}, - {new IssueChange(RuleType.BUG, DefaultTransitions.WONT_FIX)}, - {new IssueChange(RuleType.VULNERABILITY, DefaultTransitions.WONT_FIX)}, - {new IssueChange(RuleType.CODE_SMELL, DefaultTransitions.WONT_FIX)}, - {new IssueChange(DefaultTransitions.REOPEN)}, - {new IssueChange(RuleType.BUG, DefaultTransitions.REOPEN)}, - {new IssueChange(RuleType.VULNERABILITY, DefaultTransitions.REOPEN)}, - {new IssueChange(RuleType.CODE_SMELL, DefaultTransitions.REOPEN)} - }; - } - - private QGChangeEventFactory.IssueChangeData issueChangeData() { - return new QGChangeEventFactory.IssueChangeData(emptyList(), emptyList()); - } - - private QGChangeEventFactory.IssueChangeData issueChangeData(IssueDto issueDto) { - return new QGChangeEventFactory.IssueChangeData(singletonList(issueDto.toDefaultIssue()), emptyList()); - } - - private QGChangeEventFactory.IssueChangeData issueChangeData(Collection issueDtos, ComponentAndBranch... components) { - return new QGChangeEventFactory.IssueChangeData( - issueDtos.stream().map(IssueDto::toDefaultIssue).collect(Collectors.toList()), - Arrays.stream(components).map(ComponentAndBranch::getComponent).collect(Collectors.toList())); - } - - private IssueDto newIssueDto(@Nullable ComponentAndBranch projectAndBranch) { - return projectAndBranch == null ? newIssueDto() : newIssueDto(projectAndBranch.component, projectAndBranch.component); - } - - private IssueDto newIssueDto(ComponentDto componentDto) { - return newIssueDto(componentDto, componentDto); - } - - private IssueDto newIssueDto() { - return newIssueDto((ComponentDto) null, (ComponentDto) null); - } - - private IssueDto newIssueDto(@Nullable ComponentDto component, @Nullable ComponentAndBranch componentAndBranch) { - return newIssueDto(component, componentAndBranch == null ? null : componentAndBranch.component); - } - - private IssueDto newIssueDto(@Nullable ComponentDto component, @Nullable ComponentDto project) { - RuleType randomRuleType = RuleType.values()[random.nextInt(RuleType.values().length)]; - String randomStatus = Issue.STATUSES.get(random.nextInt(Issue.STATUSES.size())); - IssueDto res = new IssueDto() - .setType(randomRuleType) - .setStatus(randomStatus) - .setRuleKey(randomAlphanumeric(3), randomAlphanumeric(4)); - if (component != null) { - res.setComponent(component); - } - if (project != null) { - res.setProject(project); - } - return res; - } -} diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventFactoryTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventFactoryTest.java deleted file mode 100644 index 62d95d86768..00000000000 --- a/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventFactoryTest.java +++ /dev/null @@ -1,88 +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 org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.sonar.api.rules.RuleType; - -import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; -import static org.assertj.core.api.Assertions.assertThat; - -public class QGChangeEventFactoryTest { - @Rule - public ExpectedException expectedException = ExpectedException.none(); - - @Test - public void IssueChange_constructor_throws_IAE_if_both_args_are_null() { - expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("At least one of ruleType and transitionKey must be non null"); - - new QGChangeEventFactory.IssueChange(null, null); - } - - @Test - public void verify_IssueChange_getters() { - QGChangeEventFactory.IssueChange transitionKeyOnly = new QGChangeEventFactoryImpl.IssueChange("foo"); - assertThat(transitionKeyOnly.getTransitionKey()).contains("foo"); - assertThat(transitionKeyOnly.getRuleType()).isEmpty(); - QGChangeEventFactory.IssueChange ruleTypeOnly = new QGChangeEventFactoryImpl.IssueChange(RuleType.BUG); - assertThat(ruleTypeOnly.getTransitionKey()).isEmpty(); - assertThat(ruleTypeOnly.getRuleType()).contains(RuleType.BUG); - QGChangeEventFactory.IssueChange transitionKeyAndRuleType = new QGChangeEventFactoryImpl.IssueChange(RuleType.VULNERABILITY, "bar"); - assertThat(transitionKeyAndRuleType.getTransitionKey()).contains("bar"); - assertThat(transitionKeyAndRuleType.getRuleType()).contains(RuleType.VULNERABILITY); - } - - @Test - public void verify_IssueChange_equality() { - QGChangeEventFactory.IssueChange underTest = new QGChangeEventFactory.IssueChange(RuleType.BUG); - - assertThat(underTest).isEqualTo(new QGChangeEventFactory.IssueChange(RuleType.BUG)); - assertThat(underTest).isEqualTo(new QGChangeEventFactory.IssueChange(RuleType.BUG, null)); - - assertThat(underTest).isNotEqualTo(null); - assertThat(underTest).isNotEqualTo(new QGChangeEventFactory.IssueChange(RuleType.BUG, randomAlphanumeric(10))); - assertThat(underTest).isNotEqualTo(new QGChangeEventFactory.IssueChange(RuleType.CODE_SMELL)); - assertThat(underTest).isNotEqualTo(new QGChangeEventFactory.IssueChange(RuleType.CODE_SMELL, randomAlphanumeric(10))); - assertThat(underTest).isNotEqualTo(new QGChangeEventFactory.IssueChange(RuleType.VULNERABILITY)); - assertThat(underTest).isNotEqualTo(new QGChangeEventFactory.IssueChange(RuleType.VULNERABILITY, randomAlphanumeric(10))); - assertThat(underTest).isNotEqualTo(new QGChangeEventFactory.IssueChange(randomAlphanumeric(10))); - - String transitionKey = randomAlphanumeric(10); - underTest = new QGChangeEventFactory.IssueChange(transitionKey); - - assertThat(underTest).isEqualTo(new QGChangeEventFactory.IssueChange(transitionKey)); - assertThat(underTest).isEqualTo(new QGChangeEventFactory.IssueChange(null, transitionKey)); - - assertThat(underTest).isNotEqualTo(null); - assertThat(underTest).isNotEqualTo(new QGChangeEventFactory.IssueChange(RuleType.BUG)); - assertThat(underTest).isNotEqualTo(new QGChangeEventFactory.IssueChange(RuleType.BUG, transitionKey)); - assertThat(underTest).isNotEqualTo(new QGChangeEventFactory.IssueChange(RuleType.BUG, randomAlphanumeric(10))); - assertThat(underTest).isNotEqualTo(new QGChangeEventFactory.IssueChange(RuleType.CODE_SMELL)); - assertThat(underTest).isNotEqualTo(new QGChangeEventFactory.IssueChange(RuleType.CODE_SMELL, randomAlphanumeric(10))); - assertThat(underTest).isNotEqualTo(new QGChangeEventFactory.IssueChange(RuleType.CODE_SMELL, transitionKey)); - assertThat(underTest).isNotEqualTo(new QGChangeEventFactory.IssueChange(RuleType.VULNERABILITY)); - assertThat(underTest).isNotEqualTo(new QGChangeEventFactory.IssueChange(RuleType.VULNERABILITY, randomAlphanumeric(10))); - assertThat(underTest).isNotEqualTo(new QGChangeEventFactory.IssueChange(RuleType.VULNERABILITY, transitionKey)); - assertThat(underTest).isNotEqualTo(new QGChangeEventFactory.IssueChange(randomAlphanumeric(9))); - } -} diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImplTest.java index 8e729ebfe8c..47cee4474b7 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImplTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImplTest.java @@ -45,6 +45,7 @@ import org.sonar.db.component.ComponentDto; import org.sonar.server.qualitygate.changeevent.QGChangeEventListener.ChangedIssue; import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; @@ -69,9 +70,7 @@ public class QGChangeEventListenersImplTest { private String component1Uuid = RandomStringUtils.randomAlphabetic(6); private ComponentDto component1 = newComponentDto(component1Uuid); private DefaultIssue component1Issue = newDefaultIssue(component1Uuid); - private QGChangeEventFactory.IssueChangeData oneIssueOnComponent1 = new QGChangeEventFactory.IssueChangeData( - singletonList(component1Issue), - singletonList(component1)); + private List oneIssueOnComponent1 = singletonList(component1Issue); private QGChangeEvent component1QGChangeEvent = newQGChangeEvent(component1); private InOrder inOrder = Mockito.inOrder(listener1, listener2, listener3); @@ -79,37 +78,15 @@ public class QGChangeEventListenersImplTest { private QGChangeEventListenersImpl underTest = new QGChangeEventListenersImpl(new QGChangeEventListener[] {listener1, listener2, listener3}); @Test - public void broadcastOnIssueChange_has_no_effect_when_issueChangeData_is_empty() { - QGChangeEventFactory.IssueChangeData issueChangeData = new QGChangeEventFactory.IssueChangeData(emptyList(), emptyList()); - - underTest.broadcastOnIssueChange(issueChangeData, singletonList(component1QGChangeEvent)); - - verifyZeroInteractions(listener1, listener2, listener3); - } - - @Test - public void broadcastOnIssueChange_has_no_effect_when_issueChangeData_has_no_issue() { - QGChangeEventFactory.IssueChangeData issueChangeData = new QGChangeEventFactory.IssueChangeData( - emptyList(), singletonList(newComponentDto(component1Uuid))); - - underTest.broadcastOnIssueChange(issueChangeData, singletonList(component1QGChangeEvent)); - - verifyZeroInteractions(listener1, listener2, listener3); - } - - @Test - public void broadcastOnIssueChange_has_no_effect_when_issueChangeData_has_no_component() { - QGChangeEventFactory.IssueChangeData issueChangeData = new QGChangeEventFactory.IssueChangeData( - singletonList(newDefaultIssue(component1Uuid)), emptyList()); - - underTest.broadcastOnIssueChange(issueChangeData, singletonList(component1QGChangeEvent)); + public void broadcastOnIssueChange_has_no_effect_when_issues_are_empty() { + underTest.broadcastOnIssueChange(emptyList(), singletonList(component1QGChangeEvent)); verifyZeroInteractions(listener1, listener2, listener3); } @Test public void broadcastOnIssueChange_has_no_effect_when_no_changeEvent() { - underTest.broadcastOnIssueChange(oneIssueOnComponent1, Collections.emptySet()); + underTest.broadcastOnIssueChange(oneIssueOnComponent1, emptySet()); verifyZeroInteractions(listener1, listener2, listener3); } @@ -225,9 +202,7 @@ public class QGChangeEventListenersImplTest { .flatMap(s -> s) .collect(Collectors.toList()); - QGChangeEventFactory.IssueChangeData issueChangeData = new QGChangeEventFactory.IssueChangeData( - randomizedList(issues), - randomizedList(Arrays.asList(component1, component2, component3, component4))); + List changedIssues = randomizedList(issues); List qgChangeEvents = Stream.of( Stream.of(component1QGChangeEvent), Stream.of(component2QGChangeEvent), @@ -236,7 +211,7 @@ public class QGChangeEventListenersImplTest { .flatMap(s -> s) .collect(Collectors.toList()); - underTest.broadcastOnIssueChange(issueChangeData, randomizedList(qgChangeEvents)); + underTest.broadcastOnIssueChange(changedIssues, randomizedList(qgChangeEvents)); listeners.forEach(listener -> { verifyListenerCalled(listener, component1QGChangeEvent, component1Issue); diff --git a/server/sonar-server/src/test/java/org/sonar/server/settings/TestProjectConfigurationLoader.java b/server/sonar-server/src/test/java/org/sonar/server/settings/TestProjectConfigurationLoader.java new file mode 100644 index 00000000000..f1a252795af --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/settings/TestProjectConfigurationLoader.java @@ -0,0 +1,45 @@ +/* + * 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.settings; + +import java.util.HashMap; +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; + +public class TestProjectConfigurationLoader implements ProjectConfigurationLoader { + + private final Configuration config; + + public TestProjectConfigurationLoader(Configuration config) { + this.config = config; + } + + @Override + public Map loadProjectConfigurations(DbSession dbSession, Set projects) { + Map map = new HashMap<>(); + for (ComponentDto project : projects) { + map.put(project.uuid(), config); + } + return map; + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/webhook/ProjectAnalysisTest.java b/server/sonar-server/src/test/java/org/sonar/server/webhook/ProjectAnalysisTest.java index 1fcc474f594..21c7cb87390 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/webhook/ProjectAnalysisTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/webhook/ProjectAnalysisTest.java @@ -24,6 +24,7 @@ import java.util.Map; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.sonar.api.measures.Metric; import org.sonar.server.qualitygate.EvaluatedQualityGate; import org.sonar.server.qualitygate.QualityGate; @@ -41,7 +42,7 @@ public class ProjectAnalysisTest { private final Branch branch = new Branch(true, "name", Branch.Type.SHORT); private final EvaluatedQualityGate qualityGate = EvaluatedQualityGate.newBuilder() .setQualityGate(new QualityGate("id", "name", emptySet())) - .setStatus(EvaluatedQualityGate.Status.WARN) + .setStatus(Metric.Level.WARN) .build(); private final Map properties = ImmutableMap.of("a", "b"); private ProjectAnalysis underTest = new ProjectAnalysis(project, ceTask, analysis, branch, qualityGate, 1L, properties); @@ -108,7 +109,7 @@ public class ProjectAnalysisTest { assertThat(underTest).isNotEqualTo(new ProjectAnalysis(project, ceTask, analysis, branch, null, 1L, properties)); EvaluatedQualityGate otherQualityGate = EvaluatedQualityGate.newBuilder() .setQualityGate(new QualityGate("A", "B", emptySet())) - .setStatus(EvaluatedQualityGate.Status.WARN) + .setStatus(Metric.Level.WARN) .build(); assertThat(underTest).isNotEqualTo(new ProjectAnalysis(project, ceTask, analysis, branch, otherQualityGate, 1L, properties)); assertThat(underTest).isNotEqualTo(new ProjectAnalysis(project, ceTask, analysis, branch, qualityGate, null, properties)); @@ -134,7 +135,7 @@ public class ProjectAnalysisTest { assertThat(underTest.hashCode()).isNotEqualTo(new ProjectAnalysis(project, ceTask, analysis, branch, null, 1L, properties).hashCode()); EvaluatedQualityGate otherQualityGate = EvaluatedQualityGate.newBuilder() .setQualityGate(new QualityGate("A", "B", emptySet())) - .setStatus(EvaluatedQualityGate.Status.WARN) + .setStatus(Metric.Level.WARN) .build(); assertThat(underTest.hashCode()) .isNotEqualTo(new ProjectAnalysis(project, ceTask, analysis, branch, otherQualityGate, 1L, properties).hashCode()); diff --git a/server/sonar-server/src/test/java/org/sonar/server/webhook/WebhookPayloadFactoryImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/webhook/WebhookPayloadFactoryImplTest.java index 86dc92e961a..7415178a93d 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/webhook/WebhookPayloadFactoryImplTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/webhook/WebhookPayloadFactoryImplTest.java @@ -24,6 +24,7 @@ import java.util.Map; import javax.annotation.Nullable; import org.junit.Before; import org.junit.Test; +import org.sonar.api.measures.Metric; import org.sonar.api.platform.Server; import org.sonar.api.utils.System2; import org.sonar.server.qualitygate.Condition; @@ -59,7 +60,7 @@ public class WebhookPayloadFactoryImplTest { Condition condition = new Condition("coverage", Condition.Operator.GREATER_THAN, "70.0", "75.0", true); EvaluatedQualityGate gate = EvaluatedQualityGate.newBuilder() .setQualityGate(new QualityGate("G1", "Gate One", singleton(condition))) - .setStatus(EvaluatedQualityGate.Status.WARN) + .setStatus(Metric.Level.WARN) .addCondition(condition, EvaluatedCondition.EvaluationStatus.WARN, "74.0") .build(); ProjectAnalysis analysis = newAnalysis(task, gate, null, 1_500_000_000_000L, emptyMap()); @@ -105,7 +106,7 @@ public class WebhookPayloadFactoryImplTest { Condition condition = new Condition("coverage", Condition.Operator.GREATER_THAN, "70.0", "75.0", false); EvaluatedQualityGate gate = EvaluatedQualityGate.newBuilder() .setQualityGate(new QualityGate("G1", "Gate One", singleton(condition))) - .setStatus(EvaluatedQualityGate.Status.WARN) + .setStatus(Metric.Level.WARN) .addCondition(condition, EvaluatedCondition.EvaluationStatus.NO_VALUE, null) .build(); ProjectAnalysis analysis = newAnalysis(task, gate, null, 1_500_000_000_000L, emptyMap()); @@ -146,7 +147,7 @@ public class WebhookPayloadFactoryImplTest { CeTask task = new CeTask("#1", CeTask.Status.SUCCESS); EvaluatedQualityGate gate = EvaluatedQualityGate.newBuilder() .setQualityGate(new QualityGate("G1", "Gate One", emptySet())) - .setStatus(EvaluatedQualityGate.Status.WARN) + .setStatus(Metric.Level.WARN) .build(); Map scannerProperties = ImmutableMap.of( "sonar.analysis.revision", "ab45d24", diff --git a/server/sonar-server/src/test/java/org/sonar/server/webhook/WebhookQGChangeEventListenerTest.java b/server/sonar-server/src/test/java/org/sonar/server/webhook/WebhookQGChangeEventListenerTest.java index a8d82fc9770..1f0bb7d2e7f 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/webhook/WebhookQGChangeEventListenerTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/webhook/WebhookQGChangeEventListenerTest.java @@ -32,6 +32,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Matchers; import org.mockito.Mockito; import org.sonar.api.config.Configuration; +import org.sonar.api.measures.Metric; import org.sonar.api.utils.System2; import org.sonar.core.util.UuidFactoryFast; import org.sonar.db.DbClient; @@ -67,7 +68,7 @@ public class WebhookQGChangeEventListenerTest { private static final EvaluatedQualityGate EVALUATED_QUALITY_GATE_1 = EvaluatedQualityGate.newBuilder() .setQualityGate(new QualityGate(valueOf(ShortLivingBranchQualityGate.ID), ShortLivingBranchQualityGate.NAME, emptySet())) - .setStatus(EvaluatedQualityGate.Status.OK) + .setStatus(Metric.Level.OK) .build(); private static final Set CHANGED_ISSUES_ARE_IGNORED = emptySet(); diff --git a/server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhookDeliveriesActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhookDeliveriesActionTest.java index a49233c995a..14d91c65782 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhookDeliveriesActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhookDeliveriesActionTest.java @@ -82,7 +82,7 @@ public class WebhookDeliveriesActionTest { } @Test - public void search_by_component_and_return_no_records() throws Exception { + public void search_by_component_and_return_no_records() { userSession.logIn().addProjectPermission(UserRole.ADMIN, project); Webhooks.DeliveriesWsResponse response = ws.newRequest() @@ -93,7 +93,7 @@ public class WebhookDeliveriesActionTest { } @Test - public void search_by_task_and_return_no_records() throws Exception { + public void search_by_task_and_return_no_records() { userSession.logIn().addProjectPermission(UserRole.ADMIN, project); Webhooks.DeliveriesWsResponse response = ws.newRequest() @@ -104,7 +104,7 @@ public class WebhookDeliveriesActionTest { } @Test - public void search_by_component_and_return_records_of_example() throws Exception { + public void search_by_component_and_return_records_of_example() { WebhookDeliveryDto dto = newWebhookDeliveryDto() .setUuid("d1") .setComponentUuid(project.uuid()) @@ -128,7 +128,7 @@ public class WebhookDeliveriesActionTest { } @Test - public void search_by_task_and_return_records() throws Exception { + public void search_by_task_and_return_records() { WebhookDeliveryDto dto1 = newWebhookDeliveryDto().setComponentUuid(project.uuid()).setCeTaskUuid("t1"); WebhookDeliveryDto dto2 = newWebhookDeliveryDto().setComponentUuid(project.uuid()).setCeTaskUuid("t1"); WebhookDeliveryDto dto3 = newWebhookDeliveryDto().setComponentUuid(project.uuid()).setCeTaskUuid("t2"); @@ -146,7 +146,7 @@ public class WebhookDeliveriesActionTest { } @Test - public void search_by_component_and_throw_ForbiddenException_if_not_admin_of_project() throws Exception { + public void search_by_component_and_throw_ForbiddenException_if_not_admin_of_project() { WebhookDeliveryDto dto = newWebhookDeliveryDto() .setComponentUuid(project.uuid()); dbClient.webhookDeliveryDao().insert(db.getSession(), dto); @@ -162,7 +162,7 @@ public class WebhookDeliveriesActionTest { } @Test - public void search_by_task_and_throw_ForbiddenException_if_not_admin_of_project() throws Exception { + public void search_by_task_and_throw_ForbiddenException_if_not_admin_of_project() { WebhookDeliveryDto dto = newWebhookDeliveryDto() .setComponentUuid(project.uuid()); dbClient.webhookDeliveryDao().insert(db.getSession(), dto); @@ -178,7 +178,7 @@ public class WebhookDeliveriesActionTest { } @Test - public void throw_IAE_if_both_component_and_task_parameters_are_set() throws Exception { + public void throw_IAE_if_both_component_and_task_parameters_are_set() { userSession.logIn(); expectedException.expect(IllegalArgumentException.class); diff --git a/server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhookDeliveryActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhookDeliveryActionTest.java index e54e9d3f8d8..dbc6db0af65 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhookDeliveryActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhookDeliveryActionTest.java @@ -87,7 +87,7 @@ public class WebhookDeliveryActionTest { } @Test - public void return_404_if_delivery_does_not_exist() throws Exception { + public void return_404_if_delivery_does_not_exist() { userSession.logIn(); expectedException.expect(NotFoundException.class); @@ -99,7 +99,7 @@ public class WebhookDeliveryActionTest { } @Test - public void load_the_delivery_of_example() throws Exception { + public void load_the_delivery_of_example() { WebhookDeliveryDto dto = newWebhookDeliveryDto() .setUuid("d1") .setComponentUuid(project.uuid()) @@ -124,7 +124,7 @@ public class WebhookDeliveryActionTest { } @Test - public void return_delivery_that_failed_to_be_sent() throws Exception { + public void return_delivery_that_failed_to_be_sent() { WebhookDeliveryDto dto = newWebhookDeliveryDto() .setComponentUuid(project.uuid()) .setSuccess(false) @@ -146,7 +146,31 @@ public class WebhookDeliveryActionTest { } @Test - public void throw_ForbiddenException_if_not_admin_of_project() throws Exception { + public void return_delivery_with_none_of_optional_fields() { + WebhookDeliveryDto dto = newWebhookDeliveryDto() + .setComponentUuid(project.uuid()) + .setCeTaskUuid(null) + .setHttpStatus(null) + .setDurationMs(null) + .setErrorStacktrace(null) + .setAnalysisUuid(null); + dbClient.webhookDeliveryDao().insert(db.getSession(), dto); + db.commit(); + userSession.logIn().addProjectPermission(UserRole.ADMIN, project); + + Webhooks.DeliveryWsResponse response = ws.newRequest() + .setParam("deliveryId", dto.getUuid()) + .executeProtobuf(Webhooks.DeliveryWsResponse.class); + + Webhooks.Delivery actual = response.getDelivery(); + assertThat(actual.hasHttpStatus()).isFalse(); + assertThat(actual.hasDurationMs()).isFalse(); + assertThat(actual.hasErrorStacktrace()).isFalse(); + assertThat(actual.hasCeTaskId()).isFalse(); + } + + @Test + public void throw_ForbiddenException_if_not_admin_of_project() { WebhookDeliveryDto dto = newWebhookDeliveryDto() .setComponentUuid(project.uuid()); dbClient.webhookDeliveryDao().insert(db.getSession(), dto); diff --git a/sonar-core/src/main/java/org/sonar/core/issue/IssueChangeContext.java b/sonar-core/src/main/java/org/sonar/core/issue/IssueChangeContext.java index 2d810712ff5..702462c9f8e 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/IssueChangeContext.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/IssueChangeContext.java @@ -25,11 +25,6 @@ import java.util.Objects; import javax.annotation.CheckForNull; import javax.annotation.Nullable; -/** - * PLUGINS MUST NOT BE USED THIS CLASS, EXCEPT FOR UNIT TESTING. - * - * @since 3.6 - */ public class IssueChangeContext implements Serializable { private final String login; diff --git a/sonar-core/src/main/java/org/sonar/core/util/stream/MoreCollectors.java b/sonar-core/src/main/java/org/sonar/core/util/stream/MoreCollectors.java index 31a46f37660..d9d2e512908 100644 --- a/sonar-core/src/main/java/org/sonar/core/util/stream/MoreCollectors.java +++ b/sonar-core/src/main/java/org/sonar/core/util/stream/MoreCollectors.java @@ -25,6 +25,7 @@ import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.util.ArrayList; +import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -35,6 +36,7 @@ import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collector; +import java.util.stream.Collectors; import static java.util.Objects.requireNonNull; @@ -110,6 +112,13 @@ public final class MoreCollectors { ImmutableSet::copyOf); } + /** + * A Collector into an {@link EnumSet} of specified enumeration. + */ + public static > Collector> toEnumSet(Class enumClass) { + return Collectors.toCollection(() -> EnumSet.noneOf(enumClass)); + } + /** * Delegates to {@link java.util.stream.Collectors#toCollection(Supplier)}. */ diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index f6a72667d62..89c5ededa57 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1556,7 +1556,8 @@ metric.covered_lines.description=Covered lines metric.covered_lines.name=Covered Lines metric.critical_violations.description=Critical issues metric.critical_violations.name=Critical Issues -metric.development_cost.name=SQALE Development Cost +metric.development_cost.description=Development cost +metric.development_cost.name=Development Cost metric.directories.description=Directories metric.directories.name=Directories metric.distance.description=Distance @@ -1680,6 +1681,8 @@ metric.new_coverage.name=Coverage on New Code metric.new_coverage.short_name=Coverage metric.new_critical_violations.description=New Critical issues metric.new_critical_violations.name=New Critical Issues +metric.new_development_cost.description=Development cost on new code +metric.new_development_cost.name=Development Cost on New Code metric.new_duplicated_blocks.name=Duplicated Blocks on New Code metric.new_duplicated_blocks.extra_short_name=Duplicated Blocks metric.new_duplicated_blocks.description=Duplicated blocks on new code diff --git a/sonar-core/src/test/java/org/sonar/core/util/stream/MoreCollectorsTest.java b/sonar-core/src/test/java/org/sonar/core/util/stream/MoreCollectorsTest.java index 398d3f3756a..68c8ae6cec6 100644 --- a/sonar-core/src/test/java/org/sonar/core/util/stream/MoreCollectorsTest.java +++ b/sonar-core/src/test/java/org/sonar/core/util/stream/MoreCollectorsTest.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.EnumSet; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -101,7 +102,7 @@ public class MoreCollectorsTest { @Test public void toSet_with_size_builds_an_ImmutableSet() { - Set res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(toSet(30)); + Set res = Stream.of(1, 2, 3, 4, 5).collect(toSet(30)); assertThat(res).isInstanceOf(ImmutableSet.class) .containsExactly(1, 2, 3, 4, 5); } @@ -111,6 +112,20 @@ public class MoreCollectorsTest { assertThat(HUGE_SET.parallelStream().collect(toSet(HUGE_SET.size()))).isEqualTo(HUGE_SET); } + @Test + public void toEnumSet() { + Set res = Stream.of(MyEnum.ONE, MyEnum.ONE, MyEnum.TWO).collect(MoreCollectors.toEnumSet(MyEnum.class)); + assertThat(res).isInstanceOf(EnumSet.class) + .containsExactly(MyEnum.ONE, MyEnum.TWO); + } + + @Test + public void toEnumSet_with_empty_stream() { + Set res = Stream.empty().collect(MoreCollectors.toEnumSet(MyEnum.class)); + assertThat(res).isInstanceOf(EnumSet.class) + .isEmpty(); + } + @Test public void toArrayList_builds_an_ArrayList() { List res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(toArrayList()); @@ -516,4 +531,8 @@ public class MoreCollectorsTest { return text; } } + + private enum MyEnum { + ONE, TWO, THREE + } } diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java b/sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java index d0eccf976e8..2c1545efbd1 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java @@ -2413,8 +2413,26 @@ public final class CoreMetrics { /** * @since 4.5 */ - public static final Metric DEVELOPMENT_COST = new Metric.Builder(DEVELOPMENT_COST_KEY, "SQALE Development Cost", Metric.ValueType.STRING) - .setDescription("SQALE development cost") + public static final Metric DEVELOPMENT_COST = new Metric.Builder(DEVELOPMENT_COST_KEY, "Development Cost", Metric.ValueType.STRING) + .setDescription("Development cost") + .setDomain(DOMAIN_MAINTAINABILITY) + .setDirection(Metric.DIRECTION_WORST) + .setOptimizedBestValue(true) + .setBestValue(0.0) + .setQualitative(true) + .setHidden(true) + .create(); + + /** + * @since 7.0 + */ + public static final String NEW_DEVELOPMENT_COST_KEY = "new_development_cost"; + + /** + * @since 7.0 + */ + public static final Metric NEW_DEVELOPMENT_COST = new Metric.Builder(NEW_DEVELOPMENT_COST_KEY, "Development Cost on New Code", Metric.ValueType.STRING) + .setDescription("Development cost on new code") .setDomain(DOMAIN_MAINTAINABILITY) .setDirection(Metric.DIRECTION_WORST) .setOptimizedBestValue(true) diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/resources/Qualifiers.java b/sonar-plugin-api/src/main/java/org/sonar/api/resources/Qualifiers.java index 96d02ed89cf..8605fd9798c 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/resources/Qualifiers.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/resources/Qualifiers.java @@ -19,8 +19,12 @@ */ package org.sonar.api.resources; +import java.util.List; import java.util.Objects; +import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableList; + /** * The qualifier determines the exact type of a resource. * Plugins can define their own qualifiers. @@ -70,6 +74,15 @@ public final class Qualifiers { // ugly, should be replaced by "natures" public static final String UNIT_TEST_FILE = "UTS"; + /** + * List of qualifiers, ordered from bottom to up regarding position + * in tree of components + * + * @since 7.0 + */ + public static final List ORDERED_BOTTOM_UP = unmodifiableList(asList( + FILE, UNIT_TEST_FILE, DIRECTORY, MODULE, PROJECT, APP, SUBVIEW, VIEW)); + private Qualifiers() { // only static methods } diff --git a/tests/projects/measure/LiveMeasuresTest/sonar-project.properties b/tests/projects/measure/LiveMeasuresTest/sonar-project.properties new file mode 100644 index 00000000000..dbe1d71850c --- /dev/null +++ b/tests/projects/measure/LiveMeasuresTest/sonar-project.properties @@ -0,0 +1,5 @@ +sonar.projectKey=LiveMeasuresTestExample +sonar.projectName=LiveMeasuresTestExample +sonar.projectVersion=1.0-SNAPSHOT +sonar.sources=src +sonar.language=xoo diff --git a/tests/projects/measure/LiveMeasuresTest/src/file_with_one_line.xoo b/tests/projects/measure/LiveMeasuresTest/src/file_with_one_line.xoo new file mode 100644 index 00000000000..89b24ecec50 --- /dev/null +++ b/tests/projects/measure/LiveMeasuresTest/src/file_with_one_line.xoo @@ -0,0 +1 @@ +line 1 diff --git a/tests/projects/measure/LiveMeasuresTest/src/file_with_three_lines.xoo b/tests/projects/measure/LiveMeasuresTest/src/file_with_three_lines.xoo new file mode 100644 index 00000000000..a92d664bc20 --- /dev/null +++ b/tests/projects/measure/LiveMeasuresTest/src/file_with_three_lines.xoo @@ -0,0 +1,3 @@ +line 1 +line 2 +line 3 diff --git a/tests/src/test/java/org/sonarqube/tests/issue/IssueMeasureTest.java b/tests/src/test/java/org/sonarqube/tests/issue/IssueMeasureTest.java index 125d4475cb8..57c0aca0d3e 100644 --- a/tests/src/test/java/org/sonarqube/tests/issue/IssueMeasureTest.java +++ b/tests/src/test/java/org/sonarqube/tests/issue/IssueMeasureTest.java @@ -48,15 +48,15 @@ public class IssueMeasureTest extends AbstractIssueTest { ORCHESTRATOR.getServer().associateProjectToQualityProfile(MULTI_MODULE_SAMPLE_PROJECT_KEY, "xoo", "with-many-rules"); runProjectAnalysis(ORCHESTRATOR, "shared/xoo-multi-modules-sample"); - assertThat(search(IssueQuery.create().componentRoots(MULTI_MODULE_SAMPLE_PROJECT_KEY)).paging().total()).isEqualTo(136); + assertThat(search(IssueQuery.create().componentRoots(MULTI_MODULE_SAMPLE_PROJECT_KEY)).paging().total()).isEqualTo(128); Map measures = getMeasuresAsDoubleByMetricKey(ORCHESTRATOR, MULTI_MODULE_SAMPLE_PROJECT_KEY, "violations", "info_violations", "minor_violations", "major_violations", "blocker_violations", "critical_violations"); - assertThat(measures.get("violations")).isEqualTo(136); + assertThat(measures.get("violations")).isEqualTo(128); assertThat(measures.get("info_violations")).isEqualTo(2); assertThat(measures.get("minor_violations")).isEqualTo(61); - assertThat(measures.get("major_violations")).isEqualTo(65); + assertThat(measures.get("major_violations")).isEqualTo(57); assertThat(measures.get("blocker_violations")).isEqualTo(4); assertThat(measures.get("critical_violations")).isEqualTo(4); } diff --git a/tests/src/test/java/org/sonarqube/tests/issue/IssueSearchTest.java b/tests/src/test/java/org/sonarqube/tests/issue/IssueSearchTest.java index b8f96966eb1..0f2d03d7aac 100644 --- a/tests/src/test/java/org/sonarqube/tests/issue/IssueSearchTest.java +++ b/tests/src/test/java/org/sonarqube/tests/issue/IssueSearchTest.java @@ -55,7 +55,7 @@ public class IssueSearchTest extends AbstractIssueTest { private static final String PROJECT_KEY2 = "com.sonarsource.it.samples:multi-modules-sample2"; private static int DEFAULT_PAGINATED_RESULTS = 100; - private static int TOTAL_NB_ISSUES = 272; + private static int TOTAL_NB_ISSUES = 256; @BeforeClass public static void prepareData() { @@ -92,15 +92,15 @@ public class IssueSearchTest extends AbstractIssueTest { @Test public void search_issues_by_component_roots() { assertSearch(r -> r.setComponentRoots("com.sonarsource.it.samples:multi-modules-sample")).hasSize(DEFAULT_PAGINATED_RESULTS); - assertSearch(r -> r.setComponentRoots("com.sonarsource.it.samples:multi-modules-sample:module_a")).hasSize(82); - assertSearch(r -> r.setComponentRoots("com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1")).hasSize(36); + assertSearch(r -> r.setComponentRoots("com.sonarsource.it.samples:multi-modules-sample:module_a")).hasSize(76); + assertSearch(r -> r.setComponentRoots("com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1")).hasSize(35); assertThat(search(IssueQuery.create().componentRoots("unknown")).list()).isEmpty(); } @Test public void search_issues_by_components() { - assertSearch(r -> r.setComponents("com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo")).hasSize(34); + assertSearch(r -> r.setComponents("com.sonarsource.it.samples:multi-modules-sample:module_a:module_a1:src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo")).hasSize(33); assertSearch(r -> r.setComponents("unknown")).isEmpty(); } @@ -261,7 +261,7 @@ public class IssueSearchTest extends AbstractIssueTest { @Test public void search_issues_by_types() throws IOException { assertThat(searchResponse(r -> r.setTypes(singletonList("CODE_SMELL"))).getPaging().getTotal()).isEqualTo(142); - assertThat(searchResponse(r -> r.setTypes(singletonList("BUG"))).getPaging().getTotal()).isEqualTo(122); + assertThat(searchResponse(r -> r.setTypes(singletonList("BUG"))).getPaging().getTotal()).isEqualTo(106); assertThat(searchResponse(r -> r.setTypes(singletonList("VULNERABILITY"))).getPaging().getTotal()).isEqualTo(8); } diff --git a/tests/src/test/java/org/sonarqube/tests/measure/LiveMeasuresTest.java b/tests/src/test/java/org/sonarqube/tests/measure/LiveMeasuresTest.java new file mode 100644 index 00000000000..a47587aa4b3 --- /dev/null +++ b/tests/src/test/java/org/sonarqube/tests/measure/LiveMeasuresTest.java @@ -0,0 +1,198 @@ +/* + * 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.sonarqube.tests.measure; + +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.Table; +import com.sonar.orchestrator.Orchestrator; +import com.sonar.orchestrator.build.SonarScanner; +import java.util.List; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.sonarqube.qa.util.Tester; +import org.sonarqube.ws.Issues; +import org.sonarqube.ws.Measures; +import org.sonarqube.ws.Projects.CreateWsResponse.Project; +import org.sonarqube.ws.Qualitygates; +import org.sonarqube.ws.client.issues.DoTransitionRequest; +import org.sonarqube.ws.client.issues.SearchRequest; +import org.sonarqube.ws.client.issues.SetSeverityRequest; +import org.sonarqube.ws.client.issues.SetTypeRequest; +import org.sonarqube.ws.client.measures.ComponentRequest; +import org.sonarqube.ws.client.qualitygates.CreateConditionRequest; +import util.ItUtils; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +public class LiveMeasuresTest { + + private static final String PROJECT_KEY = "LiveMeasuresTestExample"; + private static final String PROJECT_DIR = "measure/LiveMeasuresTest"; + private static final String FILE_WITH_ONE_LINE_KEY = PROJECT_KEY + ":src/file_with_one_line.xoo"; + private static final String FILE_WITH_THREE_LINES_KEY = PROJECT_KEY + ":src/file_with_three_lines.xoo"; + private static final String SRC_DIR_KEY = PROJECT_KEY + ":src"; + + @ClassRule + public static Orchestrator orchestrator = MeasureSuite.ORCHESTRATOR; + + @Rule + public Tester tester = new Tester(orchestrator); + + @Test + public void refresh_measures_when_touching_ws_from_web_services() { + ItUtils.restoreProfile(orchestrator, getClass().getResource("/measure/LiveMeasuresTest/one-bug-per-line-profile.xml")); + Project project = tester.projects().provision(r -> r.setProject(PROJECT_KEY).setName("LiveMeasuresTestExample")); + orchestrator.getServer().associateProjectToQualityProfile(PROJECT_KEY, "xoo", "one-bug-per-line-profile"); + // quality gate on Security Rating: Warning when greater than A and Error when greater than C + Qualitygates.CreateResponse simple = tester.qGates().generate(); + tester.qGates().service().createCondition(new CreateConditionRequest().setGateId(String.valueOf(simple.getId())) + .setMetric("security_rating").setOp("GT").setWarning("1").setError("3")); + tester.qGates().associateProject(simple, project); + scanSample(); + + expectMeasures("bugs", 1, 3, 4, 4); + expectMeasures("vulnerabilities", 0, 0, 0, 0); + expectMeasures("violations", 1, 3, 4, 4); + // highest severity is MAJOR --> rating is C (3) + expectMeasures("reliability_rating", 3, 3, 3, 3); + // zero vulnerabilities -> rating is A (1) + expectMeasures("security_rating", 1, 1, 1, 1); + assertQualityGate("OK"); + + // mark a bug as FP + Issues.Issue issue = getFirstIssue(FILE_WITH_ONE_LINE_KEY, "BUG"); + markAsFalsePositive(issue); + expectMeasures("bugs", 0, 3, 3, 3); + expectMeasures("vulnerabilities", 0, 0, 0, 0); + expectMeasures("violations", 0, 3, 3, 3); + expectMeasures("reliability_rating", 1, 3, 3, 3); + expectMeasures("security_rating", 1, 1, 1, 1); + + // convert a bug to a vulnerability + issue = getFirstIssue(FILE_WITH_THREE_LINES_KEY, "BUG"); + convertToType(issue, "VULNERABILITY"); + expectMeasures("bugs", 0, 2, 2, 2); + expectMeasures("vulnerabilities", 0, 1, 1, 1); + expectMeasures("violations", 0, 3, 3, 3); + // highest severity of bugs is still MAJOR --> C (3) + expectMeasures("reliability_rating", 1, 3, 3, 3); + // a file has now a MAJOR vulnerability --> C (3) + expectMeasures("security_rating", 1, 3, 3, 3); + assertQualityGate("WARN"); + + // increase severity of a vulnerability to BLOCKER + issue = getFirstIssue(FILE_WITH_THREE_LINES_KEY, "VULNERABILITY"); + changeSeverity(issue, "BLOCKER"); + expectMeasures("bugs", 0, 2, 2, 2); + expectMeasures("vulnerabilities", 0, 1, 1, 1); + expectMeasures("violations", 0, 3, 3, 3); + expectMeasures("reliability_rating", 1, 3, 3, 3); + // highest severity of vulnerabilities is now BLOCKER --> security rating goes E (5) + expectMeasures("security_rating", 1, 5, 5, 5); + assertQualityGate("ERROR"); + + // no changes after new analysis + MeasuresDump dumpBeforeAnalysis = dump(); + scanSample(); + MeasuresDump dumpAfterAnalysis = dump(); + dumpAfterAnalysis.assertEquals(dumpBeforeAnalysis); + } + + private void markAsFalsePositive(Issues.Issue issue) { + tester.wsClient().issues().doTransition(new DoTransitionRequest().setIssue(issue.getKey()).setTransition("falsepositive")); + } + + private void convertToType(Issues.Issue issue, String type) { + tester.wsClient().issues().setType(new SetTypeRequest().setIssue(issue.getKey()).setType(type)); + } + + private void changeSeverity(Issues.Issue issue, String severity) { + tester.wsClient().issues().setSeverity(new SetSeverityRequest().setIssue(issue.getKey()).setSeverity(severity)); + } + + private Issues.Issue getFirstIssue(String componentKey, String type) { + return tester.wsClient().issues().search(new SearchRequest() + .setResolved("false") + .setTypes(singletonList(type)) + .setComponentKeys(singletonList(componentKey))).getIssuesList().get(0); + } + + private void scanSample() { + orchestrator.executeBuildQuietly(SonarScanner.create(ItUtils.projectDir(PROJECT_DIR))); + } + + private void expectMeasures(String metric, double fileWithOneLineExpectedValue, double fileWithThreeLinesExpectedValue, double srcDirExpectedValue, double projectExpectedValue) { + assertThat(Double.parseDouble(loadMeasure(FILE_WITH_ONE_LINE_KEY, metric).getValue())) + .as("Value of measure " + metric) + .isEqualTo(fileWithOneLineExpectedValue); + assertThat(Double.parseDouble(loadMeasure(FILE_WITH_THREE_LINES_KEY, metric).getValue())) + .as("Value of measure " + metric) + .isEqualTo(fileWithThreeLinesExpectedValue); + assertThat(Double.parseDouble(loadMeasure(SRC_DIR_KEY, metric).getValue())) + .as("Value of measure " + metric) + .isEqualTo(srcDirExpectedValue); + assertThat(Double.parseDouble(loadMeasure(PROJECT_KEY, metric).getValue())) + .as("Value of measure " + metric) + .isEqualTo(projectExpectedValue); + } + + private void assertQualityGate(String expectedQualityGate) { + assertThat(loadMeasure(PROJECT_KEY, "alert_status").getValue()).isEqualTo(expectedQualityGate); + } + + private Measures.Measure loadMeasure(String componentKey, String metricKey) { + return tester.wsClient().measures().component( + new ComponentRequest() + .setMetricKeys(singletonList(metricKey)) + .setComponent(componentKey)) + .getComponent().getMeasuresList().get(0); + } + + private MeasuresDump dump() { + MeasuresDump dump = new MeasuresDump(); + + asList(FILE_WITH_ONE_LINE_KEY, FILE_WITH_THREE_LINES_KEY, SRC_DIR_KEY, PROJECT_KEY).forEach(componentKey -> { + List measures = tester.wsClient().measures().component(new ComponentRequest() + .setComponent(componentKey) + // TODO request all metrics + .setMetricKeys(asList("bugs", "vulnerabilities", "reliability_rating", "security_rating", "sqale_rating", "major_violations", "blocker_violations")) + ).getComponent().getMeasuresList(); + measures.forEach(m -> dump.valuesByComponentAndMetric.put(m.getComponent(), m.getMetric(), m.getValue())); + }); + + return dump; + } + + private static class MeasuresDump { + private final Table valuesByComponentAndMetric = HashBasedTable.create(); + + void assertEquals(MeasuresDump dump) { + assertThat(valuesByComponentAndMetric.size()).isEqualTo(dump.valuesByComponentAndMetric.size()); + for (Table.Cell cell : valuesByComponentAndMetric.cellSet()) { + assertThat(dump.valuesByComponentAndMetric.get(cell.getRowKey(), cell.getColumnKey())) + .as("Measure " + cell.getColumnKey() + " on component " + cell.getRowKey()) + .isEqualTo(cell.getValue()); + } + } + } +} diff --git a/tests/src/test/java/org/sonarqube/tests/qualityModel/ReliabilityMeasureTest.java b/tests/src/test/java/org/sonarqube/tests/qualityModel/ReliabilityMeasureTest.java index 3c033f0acc6..2997929905c 100644 --- a/tests/src/test/java/org/sonarqube/tests/qualityModel/ReliabilityMeasureTest.java +++ b/tests/src/test/java/org/sonarqube/tests/qualityModel/ReliabilityMeasureTest.java @@ -66,11 +66,11 @@ public class ReliabilityMeasureTest { orchestrator.getServer().associateProjectToQualityProfile(PROJECT, "xoo", "with-many-rules"); orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-multi-modules-sample"))); - assertMeasures(PROJECT, 61, 305, 4); - assertMeasures(MODULE, 37, 185, 4); - assertMeasures(SUB_MODULE, 16, 80, 4); - assertMeasures(DIRECTORY, 16, 80, 4); - assertMeasures(FILE, 16, 80, 4); + assertMeasures(PROJECT, 53, 265, 4); + assertMeasures(MODULE, 31, 155, 4); + assertMeasures(SUB_MODULE, 15, 75, 4); + assertMeasures(DIRECTORY, 15, 75, 4); + assertMeasures(FILE, 15, 75, 4); } @Test diff --git a/tests/src/test/java/org/sonarqube/tests/webhook/WebhooksTest.java b/tests/src/test/java/org/sonarqube/tests/webhook/WebhooksTest.java index fcdcc89a98d..92c6bed436b 100644 --- a/tests/src/test/java/org/sonarqube/tests/webhook/WebhooksTest.java +++ b/tests/src/test/java/org/sonarqube/tests/webhook/WebhooksTest.java @@ -30,18 +30,29 @@ import org.apache.commons.lang3.StringUtils; import org.junit.After; import org.junit.Before; import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; +import org.sonarqube.qa.util.Tester; import org.sonarqube.tests.Category3Suite; +import org.sonarqube.ws.Issues.Issue; +import org.sonarqube.ws.Organizations.Organization; +import org.sonarqube.ws.Projects.CreateWsResponse.Project; +import org.sonarqube.ws.Qualitygates; +import org.sonarqube.ws.Qualityprofiles.CreateWsResponse.QualityProfile; import org.sonarqube.ws.Webhooks; import org.sonarqube.ws.client.HttpException; import org.sonarqube.ws.client.WsClient; +import org.sonarqube.ws.client.issues.BulkChangeRequest; +import org.sonarqube.ws.client.issues.SearchRequest; import org.sonarqube.ws.client.projects.DeleteRequest; +import org.sonarqube.ws.client.qualitygates.CreateConditionRequest; import org.sonarqube.ws.client.settings.ResetRequest; import org.sonarqube.ws.client.settings.SetRequest; import org.sonarqube.ws.client.webhooks.DeliveriesRequest; import org.sonarqube.ws.client.webhooks.DeliveryRequest; import util.ItUtils; +import static java.util.Collections.singletonList; import static java.util.Objects.requireNonNull; import static java.util.stream.IntStream.range; import static org.assertj.core.api.Assertions.assertThat; @@ -57,20 +68,22 @@ public class WebhooksTest { @ClassRule public static Orchestrator orchestrator = Category3Suite.ORCHESTRATOR; - @ClassRule public static ExternalServer externalServer = new ExternalServer(); + @Rule + public Tester tester = new Tester(orchestrator); + private WsClient adminWs = ItUtils.newAdminWsClient(orchestrator); @Before - public void setUp() throws Exception { + public void setUp() { externalServer.clear(); } @Before @After - public void reset() throws Exception { + public void reset() { disableGlobalWebhooks(); try { // delete project and related properties/webhook deliveries @@ -215,6 +228,48 @@ public class WebhooksTest { assertThat(getPersistedDeliveries()).isEmpty(); } + @Test + public void send_webhook_on_issue_change() throws InterruptedException { + Organization defaultOrganization = tester.organizations().getDefaultOrganization(); + Project wsProject = tester.projects().provision(r -> r.setProject(PROJECT_KEY).setName(PROJECT_NAME)); + enableProjectWebhooks(PROJECT_KEY, new Webhook("Burgr", externalServer.urlFor("/burgr"))); + // quality profile with one issue per line + QualityProfile qualityProfile = tester.qProfiles().createXooProfile(defaultOrganization); + tester.qProfiles().activateRule(qualityProfile, "xoo:OneIssuePerLine"); + tester.qProfiles().assignQProfileToProject(qualityProfile, wsProject); + // quality gate definition + Qualitygates.CreateResponse qGate = tester.qGates().generate(); + tester.qGates().service().createCondition(new CreateConditionRequest().setGateId(String.valueOf(qGate.getId())) + .setMetric("reliability_rating").setOp("GT").setError("1")); + tester.qGates().associateProject(qGate, wsProject); + // analyze project and clear first webhook + analyseProject(); + waitUntilAllWebHooksCalled(1); + externalServer.clear(); + + // change an issue to blocker bug + Issue firstIssue = tester.wsClient().issues().search(new SearchRequest()).getIssues(0); + tester.wsClient().issues().bulkChange(new BulkChangeRequest().setIssues(singletonList(firstIssue.getKey())) + .setSetSeverity(singletonList("BLOCKER")) + .setSetType(singletonList("BUG"))); + waitUntilAllWebHooksCalled(1); + + PayloadRequest request = externalServer.getPayloadRequests().get(0); + assertThat(request.getHttpHeaders().get("X-SonarQube-Project")).isEqualTo(PROJECT_KEY); + // verify content of payload + Map payload = jsonToMap(request.getJson()); + assertThat(payload.get("status")).isEqualTo("SUCCESS"); + assertThat(payload.get("analysedAt")).isNotNull(); + Map project = (Map) payload.get("project"); + assertThat(project.get("key")).isEqualTo(PROJECT_KEY); + assertThat(project.get("name")).isEqualTo(PROJECT_NAME); + assertThat(project.get("url")).isEqualTo(orchestrator.getServer().getUrl() + "/dashboard?id=" + PROJECT_KEY); + Map gate = (Map) payload.get("qualityGate"); + assertThat(gate.get("name")).isEqualTo(qGate.getName()); + assertThat(gate.get("status")).isEqualTo("ERROR"); + assertThat(gate.get("conditions")).isNotNull(); + } + private void analyseProject() { runProjectAnalysis(orchestrator, "shared/xoo-sample", "sonar.projectKey", PROJECT_KEY, diff --git a/tests/src/test/resources/measure/LiveMeasuresTest/one-bug-per-line-profile.xml b/tests/src/test/resources/measure/LiveMeasuresTest/one-bug-per-line-profile.xml new file mode 100644 index 00000000000..162413cacb9 --- /dev/null +++ b/tests/src/test/resources/measure/LiveMeasuresTest/one-bug-per-line-profile.xml @@ -0,0 +1,12 @@ + + + one-bug-per-line-profile + xoo + + + xoo + OneBugIssuePerLine + MAJOR + + + -- 2.39.5