]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10117 SONAR-1018 Update measures after relevant issue changes and send webhooks
authorSimon Brandhof <simon.brandhof@sonarsource.com>
Tue, 5 Dec 2017 17:04:06 +0000 (18:04 +0100)
committerSimon Brandhof <simon.brandhof@sonarsource.com>
Wed, 10 Jan 2018 05:48:47 +0000 (06:48 +0100)
156 files changed:
plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneBugIssuePerLineSensor.java
server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java
server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDto.java
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueGroupDto.java [new file with mode: 0644]
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java
server/sonar-db-dao/src/main/java/org/sonar/db/measure/LiveMeasureDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/metric/MetricDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/webhook/WebhookDeliveryDto.java
server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml
server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDtoTest.java
server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java
server/sonar-db-dao/src/test/java/org/sonar/db/measure/LiveMeasureDaoTest.java
server/sonar-db-dao/src/test/java/org/sonar/db/purge/PurgeDaoTest.java
server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexer.java
server/sonar-server/src/main/java/org/sonar/server/component/ws/AppAction.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/api/posttask/PostProjectAnalysisTasksExecutor.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/formula/counter/RatingValue.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/MutableQualityGateHolder.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolder.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolderImpl.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateService.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateServiceImpl.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/DebtRatingGrid.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/MaintainabilityMeasuresVisitor.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/NewMaintainabilityMeasuresVisitor.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/NewReliabilityAndSecurityRatingMeasuresVisitor.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/Rating.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingGrid.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingSettings.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/ReliabilityAndSecurityRatingMeasuresVisitor.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/LoadQualityGateStep.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/QualityGateMeasuresStep.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTask.java
server/sonar-server/src/main/java/org/sonar/server/es/ProjectIndexer.java
server/sonar-server/src/main/java/org/sonar/server/issue/AbstractChangeTagsAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/Action.java
server/sonar-server/src/main/java/org/sonar/server/issue/AssignAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/CommentAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/IssueChangePostProcessor.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/issue/IssueChangePostProcessorImpl.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/issue/IssueUpdater.java
server/sonar-server/src/main/java/org/sonar/server/issue/SetSeverityAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/SetTypeAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/TransitionAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndexer.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/AddCommentAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/AssignAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetSeverityAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTagsAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java
server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexer.java
server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueCounter.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueMetricFormula.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactory.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImpl.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveMeasureComputer.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveMeasureModule.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveQualityGateComputer.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveQualityGateComputerImpl.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/measure/live/package-info.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentAction.java
server/sonar-server/src/main/java/org/sonar/server/measure/ws/SearchAction.java
server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
server/sonar-server/src/main/java/org/sonar/server/qualitygate/Condition.java
server/sonar-server/src/main/java/org/sonar/server/qualitygate/ConditionEvaluator.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/qualitygate/EvaluatedCondition.java
server/sonar-server/src/main/java/org/sonar/server/qualitygate/EvaluatedQualityGate.java
server/sonar-server/src/main/java/org/sonar/server/qualitygate/LiveQualityGateFactory.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/qualitygate/LiveQualityGateFactoryImpl.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateConditionsUpdater.java
server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateConverter.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateEvaluator.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateEvaluatorImpl.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateFinder.java
server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateModule.java
server/sonar-server/src/main/java/org/sonar/server/qualitygate/RegisterQualityGates.java
server/sonar-server/src/main/java/org/sonar/server/qualitygate/ShortLivingBranchQualityGate.java
server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventFactory.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventFactoryImpl.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java
server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java
server/sonar-server/src/main/java/org/sonar/server/qualitygate/ws/GetByProjectAction.java
server/sonar-server/src/main/java/org/sonar/server/settings/ProjectConfigurationLoader.java
server/sonar-server/src/main/java/org/sonar/server/test/index/TestIndexer.java
server/sonar-server/src/main/java/org/sonar/server/ui/ws/ComponentAction.java
server/sonar-server/src/main/java/org/sonar/server/webhook/WebhookQGChangeEventListener.java
server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookWsSupport.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/formula/counter/RatingValueTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/measure/BestValueOptimizationTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitygate/MutableQualityGateHolderRule.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolderImplTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolderRule.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/DebtRatingGridTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/MaintainabilityMeasuresVisitorTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/NewMaintainabilityMeasuresVisitorTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/NewReliabilityAndSecurityRatingMeasuresVisitorTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingGridTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingSettingsTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/ReliabilityAndSecurityRatingMeasuresVisitorTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/LoadQualityGateStepTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTaskTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/IssueUpdaterTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/TestIssueChangePostProcessor.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/issue/ws/AddCommentActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/AssignActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/BulkChangeActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/DoTransitionActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetSeverityActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTagsActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTypeActionTest.java
server/sonar-server/src/test/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImplTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/measure/live/LiveMeasureComputerImplTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/measure/live/LiveMeasureModuleTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/measure/live/LiveQualityGateComputerImplTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/measure/live/MeasureMatrixTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/measure/live/TestIssueMetricFormulaFactory.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/qualitygate/ConditionEvaluatorTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/qualitygate/EvaluatedQualityGateTest.java
server/sonar-server/src/test/java/org/sonar/server/qualitygate/LiveQualityGateFactoryImplTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/qualitygate/QualityGateConverterTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/qualitygate/QualityGateFinderTest.java
server/sonar-server/src/test/java/org/sonar/server/qualitygate/QualityGateModuleTest.java
server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventFactoryImplTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventFactoryTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImplTest.java
server/sonar-server/src/test/java/org/sonar/server/settings/TestProjectConfigurationLoader.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/webhook/ProjectAnalysisTest.java
server/sonar-server/src/test/java/org/sonar/server/webhook/WebhookPayloadFactoryImplTest.java
server/sonar-server/src/test/java/org/sonar/server/webhook/WebhookQGChangeEventListenerTest.java
server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhookDeliveriesActionTest.java
server/sonar-server/src/test/java/org/sonar/server/webhook/ws/WebhookDeliveryActionTest.java
sonar-core/src/main/java/org/sonar/core/issue/IssueChangeContext.java
sonar-core/src/main/java/org/sonar/core/util/stream/MoreCollectors.java
sonar-core/src/main/resources/org/sonar/l10n/core.properties
sonar-core/src/test/java/org/sonar/core/util/stream/MoreCollectorsTest.java
sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java
sonar-plugin-api/src/main/java/org/sonar/api/resources/Qualifiers.java
tests/projects/measure/LiveMeasuresTest/sonar-project.properties [new file with mode: 0644]
tests/projects/measure/LiveMeasuresTest/src/file_with_one_line.xoo [new file with mode: 0644]
tests/projects/measure/LiveMeasuresTest/src/file_with_three_lines.xoo [new file with mode: 0644]
tests/src/test/java/org/sonarqube/tests/issue/IssueMeasureTest.java
tests/src/test/java/org/sonarqube/tests/issue/IssueSearchTest.java
tests/src/test/java/org/sonarqube/tests/measure/LiveMeasuresTest.java [new file with mode: 0644]
tests/src/test/java/org/sonarqube/tests/qualityModel/ReliabilityMeasureTest.java
tests/src/test/java/org/sonarqube/tests/webhook/WebhooksTest.java
tests/src/test/resources/measure/LiveMeasuresTest/one-bug-per-line-profile.xml [new file with mode: 0644]

index fffd48a4de34b8acab6adb5492c362106a4dcd3d..59574256742cb4e062d16cd1287c51f2bb9fd95c 100644 (file)
@@ -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();
     }
index 7f4cd20fcb8f964e64f2737368ee87c3f8754bc9..8c0b8822e7fa835fafebc8c38171a65e332a6a74 100644 (file)
@@ -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);
 
index 21c155333994b7747a6b3c5c9e2d78020b334fa7..9c12b9f610119c2b71fffed36a29969a2411033b 100644 (file)
@@ -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
index 25540e22faf366b603d0f1edc307bb8247306d39..75d23fd49e550c8d622913920c7ce69929d0a5c6 100644 (file)
@@ -29,9 +29,11 @@ import javax.annotation.Nullable;
 import org.apache.commons.lang.StringUtils;
 import org.apache.commons.lang.builder.ToStringBuilder;
 import org.sonar.api.resources.Scopes;
+import org.sonar.db.WildcardPosition;
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static java.lang.String.format;
+import static org.sonar.db.DaoDatabaseUtils.buildLikeValue;
 import static org.sonar.db.component.ComponentValidator.checkComponentKey;
 import static org.sonar.db.component.ComponentValidator.checkComponentName;
 import static org.sonar.db.component.DbTagsReader.readDbTags;
@@ -157,6 +159,10 @@ public class ComponentDto {
     return parent.getUuidPath() + parent.uuid() + UUID_PATH_SEPARATOR;
   }
 
+  public String getUuidPathLikeIncludingSelf() {
+    return buildLikeValue(formatUuidPathFromParent(this), WildcardPosition.AFTER);
+  }
+
   public Long getId() {
     return id;
   }
@@ -196,7 +202,7 @@ public class ComponentDto {
   /**
    * List of ancestor UUIDs, ordered by depth in tree.
    */
-  List<String> getUuidPathAsList() {
+  public List<String> getUuidPathAsList() {
     return UUID_PATH_SPLITTER.splitToList(uuidPath);
   }
 
index 2a20cc79dc8891a20ef72508ef6411834c0a042f..5570e5c8494d09342a5c41d5aa279021ab7581b2 100644 (file)
@@ -107,6 +107,10 @@ public class IssueDao implements Dao {
     return executeLargeInputs(componentUuids, mapper(dbSession)::selectOpenByComponentUuids);
   }
 
+  public Collection<IssueGroupDto> selectIssueGroupsByBaseComponent(DbSession dbSession, ComponentDto baseComponent, long leakPeriodBeginningDate) {
+    return mapper(dbSession).selectIssueGroupsByBaseComponent(baseComponent, leakPeriodBeginningDate);
+  }
+
   public void insert(DbSession session, IssueDto dto) {
     mapper(session).insert(dto);
   }
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 (file)
index 0000000..0b8e259
--- /dev/null
@@ -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;
+  }
+}
index ed0de50d9123d13641d1a5cfa8095867db88d5ab..9e6956976dd896de9d79b61f123191de7c4118cc 100644 (file)
  */
 package org.sonar.db.issue;
 
+import java.util.Collection;
 import java.util.List;
 import java.util.Set;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.session.ResultHandler;
+import org.sonar.db.component.ComponentDto;
 
 public interface IssueMapper {
 
@@ -46,4 +48,8 @@ public interface IssueMapper {
     @Param("projectUuid") String projectUuid,
     @Param("likeModuleUuidPath") String likeModuleUuidPath,
     ResultHandler<IssueDto> handler);
+
+  Collection<IssueGroupDto> selectIssueGroupsByBaseComponent(
+    @Param("baseComponent") ComponentDto baseComponent,
+    @Param("leakPeriodBeginningDate") long leakPeriodBeginningDate);
 }
index 17f3878cca94568e4f1e5833e9ee782896b95d0b..14e8636561c870d4b4e64bc229d4dfcc3736c6e0 100644 (file)
@@ -42,7 +42,7 @@ public class LiveMeasureDao implements Dao {
     this.system2 = system2;
   }
 
-  public List<LiveMeasureDto> selectByComponentUuids(DbSession dbSession, Collection<String> largeComponentUuids, Collection<Integer> metricIds) {
+  public List<LiveMeasureDto> selectByComponentUuidsAndMetricIds(DbSession dbSession, Collection<String> largeComponentUuids, Collection<Integer> metricIds) {
     if (largeComponentUuids.isEmpty() || metricIds.isEmpty()) {
       return Collections.emptyList();
     }
@@ -90,6 +90,9 @@ public class LiveMeasureDao implements Dao {
     }
   }
 
+  /**
+   * Delete the rows that do NOT have the specified marker
+   */
   public void deleteByProjectUuidExcludingMarker(DbSession dbSession, String projectUuid, String marker) {
     mapper(dbSession).deleteByProjectUuidExcludingMarker(projectUuid, marker);
   }
index 160f43505a0a8f64ebcc4043eda4b98ace8c7b70..0f27ac2bae9b9ac6817d0cc2c972e5022f7f66c9 100644 (file)
@@ -47,7 +47,7 @@ public class MetricDao implements Dao {
     return mapper(session).selectByKey(key);
   }
 
-  public List<MetricDto> selectByKeys(final DbSession session, List<String> keys) {
+  public List<MetricDto> selectByKeys(final DbSession session, Collection<String> keys) {
     return executeLargeInputs(keys, mapper(session)::selectByKeys);
   }
 
index b414ecc7e18b26b7ba921ee6f7f3480c2e0e5019..91d076cb5dc36f7d569e1a4f12b8fb83fb457fc9 100644 (file)
@@ -39,12 +39,11 @@ public class WebhookDeliveryDto extends WebhookDeliveryLiteDto<WebhookDeliveryDt
     return this;
   }
 
-  @CheckForNull
   public String getPayload() {
     return payload;
   }
 
-  public WebhookDeliveryDto setPayload(@Nullable String s) {
+  public WebhookDeliveryDto setPayload(String s) {
     this.payload = s;
     return this;
   }
index 545963ea5e593f21b14723d4426e7504e8c3c90d..a077a1e17caa42d435c3b3190500829e8b2badc2 100644 (file)
     p.module_uuid_path like  #{likeModuleUuidPath, jdbcType=VARCHAR} escape '/' and
     i.status &lt;&gt; 'CLOSED'
   </select>
+
+  <select id="selectIssueGroupsByBaseComponent" resultType="org.sonar.db.issue.IssueGroupDto" parameterType="map">
+    select i.issue_type as ruleType, i.severity as severity, i.resolution as resolution, i.status as status, sum(i.effort) as effort, count(i.issue_type) as "count", (i.issue_creation_date &gt;= #{leakPeriodBeginningDate,jdbcType=BIGINT}) as inLeak
+    from issues i
+    inner join projects p on p.uuid = i.component_uuid and p.project_uuid = i.project_uuid
+    where i.status !='CLOSED'
+    and i.project_uuid = #{baseComponent.projectUuid,jdbcType=VARCHAR}
+    and (p.uuid_path like #{baseComponent.uuidPathLikeIncludingSelf,jdbcType=VARCHAR} escape '/' or p.uuid = #{baseComponent.uuid,jdbcType=VARCHAR})
+    group by i.issue_type, i.severity, i.resolution, i.status, inLeak
+  </select>
+
+  <select id="selectIssueGroupsByBaseComponent" resultType="org.sonar.db.issue.IssueGroupDto" parameterType="map" databaseId="oracle">
+    select i2.issue_type as ruleType, i2.severity as severity, i2.resolution as resolution, i2.status as status, sum(i2.effort) as effort, count(i2.issue_type) as "count", i2.inLeak as inLeak
+    from (
+      select i.issue_type, i.severity, i.resolution, i.status, i.effort, case when i.issue_creation_date &gt; #{leakPeriodBeginningDate,jdbcType=BIGINT} then 1 else 0 end as inLeak
+      from issues i
+      inner join projects p on p.uuid = i.component_uuid and p.project_uuid = i.project_uuid
+      where i.status !='CLOSED'
+      and i.project_uuid = #{baseComponent.projectUuid,jdbcType=VARCHAR}
+      and (p.uuid_path like #{baseComponent.uuidPathLikeIncludingSelf,jdbcType=VARCHAR} escape '/' or p.uuid = #{baseComponent.uuid,jdbcType=VARCHAR})
+    ) i2
+    group by i2.issue_type, i2.severity, i2.resolution, i2.status, i2.inLeak
+  </select>
+
+  <select id="selectIssueGroupsByBaseComponent" resultType="org.sonar.db.issue.IssueGroupDto" parameterType="map" databaseId="mssql">
+    select i2.issue_type as ruleType, i2.severity as severity, i2.resolution as resolution, i2.status as status, sum(i2.effort) as effort, count(i2.issue_type) as "count", i2.inLeak as inLeak
+    from (
+    select i.issue_type, i.severity, i.resolution, i.status, i.effort, case when i.issue_creation_date &gt; #{leakPeriodBeginningDate,jdbcType=BIGINT} then 1 else 0 end as inLeak
+    from issues i
+    inner join projects p on p.uuid = i.component_uuid and p.project_uuid = i.project_uuid
+    where i.status !='CLOSED'
+    and i.project_uuid = #{baseComponent.projectUuid,jdbcType=VARCHAR}
+    and (p.uuid_path like #{baseComponent.uuidPathLikeIncludingSelf,jdbcType=VARCHAR} escape '/' or p.uuid = #{baseComponent.uuid,jdbcType=VARCHAR})
+    ) i2
+    group by i2.issue_type, i2.severity, i2.resolution, i2.status, i2.inLeak
+  </select>
 </mapper>
 
index 92a9ef6a6647185ab9eb4e65d565ed16626296c5..709308d9ce6ad827566a180dd5b5a56a6cbb5150 100644 (file)
@@ -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");
index 1b948c72a653052cc044a4da7f054396d1aacce8..c32c54e366a94fca506bf4f2afebbe684ce96d66 100644 (file)
@@ -21,6 +21,7 @@ package org.sonar.db.issue;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import org.apache.ibatis.session.ResultContext;
@@ -30,6 +31,7 @@ import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.sonar.api.issue.Issue;
 import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.RuleType;
 import org.sonar.api.utils.System2;
 import org.sonar.db.DbTester;
 import org.sonar.db.RowNotFoundException;
@@ -234,6 +236,59 @@ public class IssueDaoTest {
     assertThat(fp.getIssueCreationDate()).isNotNull();
   }
 
+  @Test
+  public void test_selectGroupsOfComponentTreeOnLeak_on_component_without_issues() {
+    ComponentDto project = db.components().insertPublicProject();
+    ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
+
+    Collection<IssueGroupDto> groups = underTest.selectIssueGroupsByBaseComponent(db.getSession(), file, 1_000L);
+
+    assertThat(groups).isEmpty();
+  }
+
+  @Test
+  public void selectGroupsOfComponentTreeOnLeak_on_file() {
+    ComponentDto project = db.components().insertPublicProject();
+    ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
+    RuleDefinitionDto rule = db.rules().insert();
+    IssueDto fpBug = db.issues().insert(rule, project, file,
+      i -> i.setStatus("RESOLVED").setResolution("FALSE-POSITIVE").setSeverity("MAJOR").setType(RuleType.BUG).setIssueCreationTime(1_500L));
+    IssueDto criticalBug1 = db.issues().insert(rule, project, file,
+      i -> i.setStatus("OPEN").setResolution(null).setSeverity("CRITICAL").setType(RuleType.BUG).setIssueCreationTime(1_600L));
+    IssueDto criticalBug2 = db.issues().insert(rule, project, file,
+      i -> i.setStatus("OPEN").setResolution(null).setSeverity("CRITICAL").setType(RuleType.BUG).setIssueCreationTime(1_700L));
+    // closed issues are ignored
+    IssueDto closed = db.issues().insert(rule, project, file,
+      i -> i.setStatus("CLOSED").setResolution("REMOVED").setSeverity("CRITICAL").setType(RuleType.BUG).setIssueCreationTime(1_700L));
+
+    Collection<IssueGroupDto> result = underTest.selectIssueGroupsByBaseComponent(db.getSession(), file, 1_000L);
+
+    assertThat(result.stream().mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(3);
+
+    assertThat(result.stream().filter(g -> g.getRuleType()==RuleType.BUG.getDbConstant()).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(3);
+    assertThat(result.stream().filter(g -> g.getRuleType()==RuleType.CODE_SMELL.getDbConstant()).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(0);
+    assertThat(result.stream().filter(g -> g.getRuleType()==RuleType.VULNERABILITY.getDbConstant()).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(0);
+
+    assertThat(result.stream().filter(g -> g.getSeverity().equals("CRITICAL")).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(2);
+    assertThat(result.stream().filter(g -> g.getSeverity().equals("MAJOR")).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(1);
+    assertThat(result.stream().filter(g -> g.getSeverity().equals("MINOR")).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(0);
+
+    assertThat(result.stream().filter(g -> g.getStatus().equals("OPEN")).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(2);
+    assertThat(result.stream().filter(g -> g.getStatus().equals("RESOLVED")).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(1);
+    assertThat(result.stream().filter(g -> g.getStatus().equals("CLOSED")).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(0);
+
+    assertThat(result.stream().filter(g -> "FALSE-POSITIVE" .equals(g.getResolution())).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(1);
+    assertThat(result.stream().filter(g -> g.getResolution()== null).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(2);
+
+    assertThat(result.stream().filter(g -> g.isInLeak()).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(3);
+    assertThat(result.stream().filter(g -> !g.isInLeak()).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(0);
+
+    // test leak
+    result = underTest.selectIssueGroupsByBaseComponent(db.getSession(), file, 999_999_999L);
+    assertThat(result.stream().filter(g -> g.isInLeak()).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(0);
+    assertThat(result.stream().filter(g -> !g.isInLeak()).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(3);
+  }
+
   private static IssueDto newIssueDto(String key) {
     IssueDto dto = new IssueDto();
     dto.setComponent(new ComponentDto().setDbKey("struts:Action").setId(123L).setUuid("component-uuid"));
index de232ef5a43eab7d54252270e3c3614b4a394354..a37fc2ce3b3f4aee3e4479616975bc6ad82b1c71 100644 (file)
  */
 package org.sonar.db.measure;
 
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
 import java.util.List;
-import org.assertj.core.groups.Tuple;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.sonar.api.utils.System2;
 import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDto;
 import org.sonar.db.metric.MetricDto;
 
 import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singleton;
 import static java.util.Collections.singletonList;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.groups.Tuple.tuple;
+import static org.sonar.db.component.ComponentTesting.newFileDto;
+import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto;
 import static org.sonar.db.measure.MeasureTesting.newLiveMeasure;
 
 public class LiveMeasureDaoTest {
 
-  private static final int A_METRIC_ID = 42;
-
   @Rule
   public DbTester db = DbTester.create(System2.INSTANCE);
 
   private LiveMeasureDao underTest = db.getDbClient().liveMeasureDao();
+  private MetricDto metric;
+
+  @Before
+  public void setUp() throws Exception {
+    metric = db.measures().insertMetric();
+  }
 
   @Test
-  public void test_selectByComponentUuids() {
-    LiveMeasureDto measure1 = newLiveMeasure().setMetricId(A_METRIC_ID);
-    LiveMeasureDto measure2 = newLiveMeasure().setMetricId(A_METRIC_ID);
+  public void test_selectByComponentUuidsAndMetricIds() {
+    LiveMeasureDto measure1 = newLiveMeasure().setMetricId(metric.getId());
+    LiveMeasureDto measure2 = newLiveMeasure().setMetricId(metric.getId());
     underTest.insert(db.getSession(), measure1);
     underTest.insert(db.getSession(), measure2);
 
-    List<LiveMeasureDto> selected = underTest.selectByComponentUuids(db.getSession(), asList(measure1.getComponentUuid(), measure2.getComponentUuid()), singletonList(A_METRIC_ID));
+    List<LiveMeasureDto> selected = underTest.selectByComponentUuidsAndMetricIds(db.getSession(),
+      asList(measure1.getComponentUuid(), measure2.getComponentUuid()), singletonList(metric.getId()));
     assertThat(selected)
       .extracting(LiveMeasureDto::getComponentUuid, LiveMeasureDto::getProjectUuid, LiveMeasureDto::getMetricId, LiveMeasureDto::getValue, LiveMeasureDto::getDataAsString)
       .containsExactlyInAnyOrder(
-        Tuple.tuple(measure1.getComponentUuid(), measure1.getProjectUuid(), measure1.getMetricId(), measure1.getValue(), measure1.getDataAsString()),
-        Tuple.tuple(measure2.getComponentUuid(), measure2.getProjectUuid(), measure2.getMetricId(), measure2.getValue(), measure2.getDataAsString()));
+        tuple(measure1.getComponentUuid(), measure1.getProjectUuid(), measure1.getMetricId(), measure1.getValue(), measure1.getDataAsString()),
+        tuple(measure2.getComponentUuid(), measure2.getProjectUuid(), measure2.getMetricId(), measure2.getValue(), measure2.getDataAsString()));
+
+    assertThat(underTest.selectByComponentUuidsAndMetricIds(db.getSession(), emptyList(), singletonList(metric.getId()))).isEmpty();
+    assertThat(underTest.selectByComponentUuidsAndMetricIds(db.getSession(), singletonList(measure1.getComponentUuid()), emptyList())).isEmpty();
   }
 
   @Test
-  public void selectByComponentUuids_returns_empty_list_if_metric_does_not_match() {
-    LiveMeasureDto measure = newLiveMeasure().setMetricId(10);
+  public void selectByComponentUuidsAndMetricIds_returns_empty_list_if_metric_does_not_match() {
+    LiveMeasureDto measure = newLiveMeasure().setMetricId(metric.getId());
     underTest.insert(db.getSession(), measure);
 
-    List<LiveMeasureDto> selected = underTest.selectByComponentUuids(db.getSession(), singletonList(measure.getComponentUuid()), singletonList(222));
+    int otherMetricId = metric.getId() + 100;
+    List<LiveMeasureDto> selected = underTest.selectByComponentUuidsAndMetricIds(db.getSession(), singletonList(measure.getComponentUuid()), singletonList(otherMetricId));
 
     assertThat(selected).isEmpty();
   }
 
   @Test
-  public void selectByComponentUuids_returns_empty_list_if_component_does_not_match() {
+  public void selectByComponentUuidsAndMetricIds_returns_empty_list_if_component_does_not_match() {
     LiveMeasureDto measure = newLiveMeasure();
     underTest.insert(db.getSession(), measure);
 
-    List<LiveMeasureDto> selected = underTest.selectByComponentUuids(db.getSession(), singletonList("_missing_"), singletonList(measure.getMetricId()));
+    List<LiveMeasureDto> selected = underTest.selectByComponentUuidsAndMetricIds(db.getSession(), singletonList("_missing_"), singletonList(measure.getMetricId()));
+
+    assertThat(selected).isEmpty();
+  }
+
+  @Test
+  public void test_selectByComponentUuidsAndMetricKeys() {
+    LiveMeasureDto measure1 = newLiveMeasure().setMetricId(metric.getId());
+    LiveMeasureDto measure2 = newLiveMeasure().setMetricId(metric.getId());
+    underTest.insert(db.getSession(), measure1);
+    underTest.insert(db.getSession(), measure2);
+
+    List<LiveMeasureDto> selected = underTest.selectByComponentUuidsAndMetricKeys(db.getSession(), asList(measure1.getComponentUuid(), measure2.getComponentUuid()),
+      singletonList(metric.getKey()));
+    assertThat(selected)
+      .extracting(LiveMeasureDto::getComponentUuid, LiveMeasureDto::getProjectUuid, LiveMeasureDto::getMetricId, LiveMeasureDto::getValue, LiveMeasureDto::getDataAsString)
+      .containsExactlyInAnyOrder(
+        tuple(measure1.getComponentUuid(), measure1.getProjectUuid(), measure1.getMetricId(), measure1.getValue(), measure1.getDataAsString()),
+        tuple(measure2.getComponentUuid(), measure2.getProjectUuid(), measure2.getMetricId(), measure2.getValue(), measure2.getDataAsString()));
+
+    assertThat(underTest.selectByComponentUuidsAndMetricKeys(db.getSession(), emptyList(), singletonList(metric.getKey()))).isEmpty();
+    assertThat(underTest.selectByComponentUuidsAndMetricKeys(db.getSession(), singletonList(measure1.getComponentUuid()), emptyList())).isEmpty();
+  }
+
+  @Test
+  public void selectByComponentUuidsAndMetricKeys_returns_empty_list_if_metric_does_not_match() {
+    LiveMeasureDto measure = newLiveMeasure().setMetricId(metric.getId());
+    underTest.insert(db.getSession(), measure);
+
+    List<LiveMeasureDto> selected = underTest.selectByComponentUuidsAndMetricKeys(db.getSession(), singletonList(measure.getComponentUuid()), singletonList("_other_"));
+
+    assertThat(selected).isEmpty();
+  }
+
+  @Test
+  public void selectByComponentUuidsAndMetricKeys_returns_empty_list_if_component_does_not_match() {
+    LiveMeasureDto measure = newLiveMeasure().setMetricId(metric.getId());
+    underTest.insert(db.getSession(), measure);
+
+    List<LiveMeasureDto> selected = underTest.selectByComponentUuidsAndMetricKeys(db.getSession(), singletonList("_missing_"), singletonList(metric.getKey()));
 
     assertThat(selected).isEmpty();
   }
@@ -96,6 +152,68 @@ public class LiveMeasureDaoTest {
       .isEqualToComparingFieldByField(stored);
   }
 
+  @Test
+  public void selectTreeByQuery() {
+    List<LiveMeasureDto> results = new ArrayList<>();
+    MetricDto metric = db.measures().insertMetric();
+    ComponentDto project = db.components().insertPrivateProject();
+    ComponentDto file = db.components().insertComponent(newFileDto(project));
+    underTest.insert(db.getSession(), newLiveMeasure(file, metric).setValue(3.14));
+
+    underTest.selectTreeByQuery(db.getSession(), project,
+      MeasureTreeQuery.builder()
+        .setMetricIds(singleton(metric.getId()))
+        .setStrategy(MeasureTreeQuery.Strategy.LEAVES).build(),
+      context -> results.add(context.getResultObject()));
+
+    assertThat(results).hasSize(1);
+    LiveMeasureDto result = results.get(0);
+    assertThat(result.getComponentUuid()).isEqualTo(file.uuid());
+    assertThat(result.getMetricId()).isEqualTo(metric.getId());
+    assertThat(result.getValue()).isEqualTo(3.14);
+  }
+
+  @Test
+  public void selectTreeByQuery_with_empty_results() {
+    List<LiveMeasureDto> results = new ArrayList<>();
+    underTest.selectTreeByQuery(db.getSession(), newPrivateProjectDto(db.getDefaultOrganization()),
+      MeasureTreeQuery.builder().setStrategy(MeasureTreeQuery.Strategy.LEAVES).build(),
+      context -> results.add(context.getResultObject()));
+
+    assertThat(results).isEmpty();
+  }
+
+  @Test
+  public void selectMeasure_map_fields() {
+    MetricDto metric = db.measures().insertMetric();
+    ComponentDto project = db.components().insertPrivateProject();
+    ComponentDto file = db.components().insertComponent(newFileDto(project));
+    underTest.insert(db.getSession(), newLiveMeasure(file, metric).setValue(3.14).setVariation(0.1).setData("text_value"));
+
+    LiveMeasureDto result = underTest.selectMeasure(db.getSession(), file.uuid(), metric.getKey()).orElseThrow(() -> new IllegalArgumentException("Measure not found"));
+
+    assertThat(result).as("Fail to map fields of %s", result.toString()).extracting(
+      LiveMeasureDto::getProjectUuid, LiveMeasureDto::getComponentUuid, LiveMeasureDto::getMetricId, LiveMeasureDto::getValue, LiveMeasureDto::getVariation,
+      LiveMeasureDto::getDataAsString, LiveMeasureDto::getTextValue)
+      .contains(project.uuid(), file.uuid(), metric.getId(), 3.14, 0.1, "text_value", "text_value");
+  }
+
+  @Test
+  public void insert_data() {
+    byte[] data = "text_value".getBytes(StandardCharsets.UTF_8);
+    MetricDto metric = db.measures().insertMetric();
+    ComponentDto project = db.components().insertPrivateProject();
+    ComponentDto file = db.components().insertComponent(newFileDto(project));
+    LiveMeasureDto measure = newLiveMeasure(file, metric).setData(data);
+
+    underTest.insert(db.getSession(), measure);
+
+
+    LiveMeasureDto result = underTest.selectMeasure(db.getSession(), file.uuid(), metric.getKey()).orElseThrow(() -> new IllegalArgumentException("Measure not found"));
+    assertThat(new String(result.getData(), StandardCharsets.UTF_8)).isEqualTo("text_value");
+    assertThat(result.getDataAsString()).isEqualTo("text_value");
+  }
+
   @Test
   public void test_insertOrUpdate() {
     // insert
@@ -139,7 +257,7 @@ public class LiveMeasureDaoTest {
   }
 
   private void verifyPersisted(LiveMeasureDto dto) {
-    List<LiveMeasureDto> selected = underTest.selectByComponentUuids(db.getSession(), singletonList(dto.getComponentUuid()), singletonList(dto.getMetricId()));
+    List<LiveMeasureDto> selected = underTest.selectByComponentUuidsAndMetricIds(db.getSession(), singletonList(dto.getComponentUuid()), singletonList(dto.getMetricId()));
     assertThat(selected).hasSize(1);
     assertThat(selected.get(0)).isEqualToComparingFieldByField(dto);
   }
index 49012759ba7d802f5f02cb15ea6d220a79229230..7a21a6b0850ddfce07863212cb88c05472c6e5e7 100644 (file)
@@ -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) {
index 71f40c3618c54bf3307ae372e738eb7cd1d356fc..86d055ccbba0ef457d0cc4a2428e4dae535cec9e 100644 (file)
@@ -84,9 +84,10 @@ public class ComponentIndexer implements ProjectIndexer, NeedAuthorizationIndexe
   @Override
   public Collection<EsQueueDto> prepareForRecovery(DbSession dbSession, Collection<String> projectUuids, Cause cause) {
     switch (cause) {
+      case MEASURE_CHANGE:
       case PROJECT_TAGS_UPDATE:
       case PERMISSION_CHANGE:
-        // tags and permissions are not part of type components/component
+        // measures, tags and permissions are not part of type components/component
         return emptyList();
 
       case PROJECT_CREATION:
index f4fa6bc5ab2cea461957d3a7caac7e0a24f9314c..562ffd31f7a636b9a6928ba305d497209b37bfce 100644 (file)
@@ -190,7 +190,7 @@ public class AppAction implements ComponentsWsAction {
     List<MetricDto> metrics = dbClient.metricDao().selectByKeys(dbSession, METRIC_KEYS);
     Map<Integer, MetricDto> metricsById = Maps.uniqueIndex(metrics, MetricDto::getId);
     List<LiveMeasureDto> measures = dbClient.liveMeasureDao()
-      .selectByComponentUuids(dbSession, Collections.singletonList(component.uuid()), metricsById.keySet());
+      .selectByComponentUuidsAndMetricIds(dbSession, Collections.singletonList(component.uuid()), metricsById.keySet());
     return Maps.uniqueIndex(measures, m -> metricsById.get(m.getMetricId()).getKey());
   }
 
index 7e2c720470d91611e56b9dfdf83f5eb55211f596..8af751c52bc781fc594dd90617a425540ad27670 100644 (file)
@@ -163,7 +163,7 @@ public class PostProjectAnalysisTasksExecutor implements ComputationStepExecutor
 
   @CheckForNull
   private QualityGateImpl createQualityGate() {
-    com.google.common.base.Optional<org.sonar.server.computation.task.projectanalysis.qualitygate.QualityGate> qualityGateOptional = this.qualityGateHolder.getQualityGate();
+    Optional<org.sonar.server.computation.task.projectanalysis.qualitygate.QualityGate> qualityGateOptional = this.qualityGateHolder.getQualityGate();
     if (qualityGateOptional.isPresent()) {
       org.sonar.server.computation.task.projectanalysis.qualitygate.QualityGate qualityGate = qualityGateOptional.get();
 
index d8e175cea19e5b0dc80a8507733e2daf22d8d3c8..03077e12a648d56a4c7d1be0a5891a2d0c3fabbf 100644 (file)
@@ -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.
index 722fd38894c232f1796406190dc85641b2656873..06f366cbb04e6a3c270c623d88dd6bd15dafe6f4 100644 (file)
  */
 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);
 }
index 86f695c865f961ca057fd0dc49b7f9f196ad76e2..ab4087034d4c67ff5f4c28dba9f40fe6656ebfda 100644 (file)
@@ -19,7 +19,8 @@
  */
 package org.sonar.server.computation.task.projectanalysis.qualitygate;
 
-import com.google.common.base.Optional;
+import java.util.Optional;
+import org.sonar.server.qualitygate.EvaluatedQualityGate;
 
 public interface QualityGateHolder {
   /**
@@ -28,4 +29,11 @@ public interface QualityGateHolder {
    * @throws IllegalStateException if the holder has not been initialized (ie. we don't know yet what is the QualityGate)
    */
   Optional<QualityGate> getQualityGate();
+
+  /**
+   * Evaluation of quality gate, including status and condition details.
+   *
+   * @throws IllegalStateException if the holder has not been initialized (ie. gate has not been evaluated yet)
+   */
+  Optional<EvaluatedQualityGate> getEvaluation();
 }
index cbba9334d4b187c8f0bb682bcd8b6356725cd7af..ca83ac5eff5b35f4bf5513d08d4f60254b0b09a4 100644 (file)
  */
 package org.sonar.server.computation.task.projectanalysis.qualitygate;
 
-import com.google.common.base.Optional;
-import javax.annotation.CheckForNull;
+import java.util.Optional;
+import org.sonar.server.qualitygate.EvaluatedQualityGate;
 
-import static com.google.common.base.Optional.absent;
-import static com.google.common.base.Optional.of;
 import static com.google.common.base.Preconditions.checkState;
 import static java.util.Objects.requireNonNull;
 
 public class QualityGateHolderImpl implements MutableQualityGateHolder {
-  private boolean initialized = false;
-  @CheckForNull
-  private Optional<QualityGate> qualityGate;
+  private QualityGate qualityGate;
+  private EvaluatedQualityGate evaluation;
 
   @Override
-  public void setQualityGate(QualityGate qualityGate) {
+  public void setQualityGate(QualityGate g) {
     // fail fast
-    requireNonNull(qualityGate);
-    checkNotInitialized();
+    requireNonNull(g);
+    checkState(qualityGate == null, "QualityGateHolder can be initialized only once");
 
-    this.initialized = true;
-    this.qualityGate = of(qualityGate);
+    this.qualityGate = g;
   }
 
   @Override
-  public void setNoQualityGate() {
-    checkNotInitialized();
-
-    this.initialized = true;
-    this.qualityGate = absent();
+  public Optional<QualityGate> getQualityGate() {
+    checkState(qualityGate != null, "QualityGate has not been set yet");
+    return Optional.of(qualityGate);
   }
 
-  private void checkNotInitialized() {
-    checkState(!initialized, "QualityGateHolder can be initialized only once");
+  @Override
+  public void setEvaluation(EvaluatedQualityGate g) {
+    // fail fast
+    requireNonNull(g);
+    checkState(evaluation == null, "QualityGateHolder evaluation can be initialized only once");
+
+    this.evaluation = g;
   }
 
   @Override
-  public Optional<QualityGate> getQualityGate() {
-    checkState(initialized, "QualityGate has not been set yet");
-    return qualityGate;
+  public Optional<EvaluatedQualityGate> getEvaluation() {
+    checkState(evaluation != null, "Evaluation of QualityGate has not been set yet");
+    return Optional.of(evaluation);
   }
 }
index c8fb00d37b0eb7ee889be1b264f3d0559647f049..580c4d58c278157dc3c3a3601840393e15ca5b87 100644 (file)
@@ -30,8 +30,9 @@ public interface QualityGateService {
   Optional<QualityGate> findById(long id);
 
   /**
-   * Retrieve the {@link QualityGate} from the database with the specified uuid, if it exists.
+   * Retrieve the {@link QualityGate} from the database with the specified uuid.
+   * @throws IllegalStateException if database is corrupted and default gate can't be found.
    */
-  Optional<QualityGate> findDefaultQualityGate(Organization organizationDto);
+  QualityGate findDefaultQualityGate(Organization organizationDto);
 
 }
index bb34218455eea43eef2cb5c64776bf43927ed72b..bb4a3e0a36aaedd071f2d292c1c11548a3597e8b 100644 (file)
@@ -57,13 +57,13 @@ public class QualityGateServiceImpl implements QualityGateService {
   }
 
   @Override
-  public Optional<QualityGate> findDefaultQualityGate(Organization organization) {
+  public QualityGate findDefaultQualityGate(Organization organization) {
     try (DbSession dbSession = dbClient.openSession(false)) {
       QualityGateDto qualityGateDto = dbClient.qualityGateDao().selectByOrganizationAndUuid(dbSession, organization.toDto(), organization.getDefaultQualityGateUuid());
       if (qualityGateDto == null) {
-        return Optional.empty();
+        throw new IllegalStateException("The default Quality gate is missing on organization " + organization.getKey());
       }
-      return Optional.of(toQualityGate(dbSession, qualityGateDto));
+      return toQualityGate(dbSession, qualityGateDto);
     }
   }
 
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/DebtRatingGrid.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/DebtRatingGrid.java
new file mode 100644 (file)
index 0000000..9114db7
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.computation.task.projectanalysis.qualitymodel;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.util.Arrays;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.utils.MessageException;
+
+import static org.sonar.api.CoreProperties.RATING_GRID;
+import static org.sonar.api.CoreProperties.RATING_GRID_DEF_VALUES;
+
+public class DebtRatingGrid {
+
+  private final double[] gridValues;
+
+  public DebtRatingGrid(double[] gridValues) {
+    this.gridValues = Arrays.copyOf(gridValues, gridValues.length);
+  }
+
+  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) {
+        return rating;
+      }
+    }
+    throw MessageException.of("The rating density value should be between 0 and " + Double.MAX_VALUE + " and got " + density);
+  }
+
+  public double getGradeLowerBound(Rating rating) {
+    if (rating.getIndex() > 1) {
+      return gridValues[rating.getIndex() - 2];
+    }
+    return 0;
+  }
+
+  @VisibleForTesting
+  double[] getGridValues() {
+    return gridValues;
+  }
+
+}
index 8670962407c5792f9bb4926110166a8c287745a2..e707df6d9fc427b4ae485b9b81f2c39499ed0b49 100644 (file)
@@ -29,7 +29,6 @@ import org.sonar.server.computation.task.projectanalysis.measure.Measure;
 import org.sonar.server.computation.task.projectanalysis.measure.MeasureRepository;
 import org.sonar.server.computation.task.projectanalysis.metric.Metric;
 import org.sonar.server.computation.task.projectanalysis.metric.MetricRepository;
-import org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating;
 
 import static org.sonar.api.measures.CoreMetrics.DEVELOPMENT_COST_KEY;
 import static org.sonar.api.measures.CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A_KEY;
@@ -50,7 +49,6 @@ import static org.sonar.server.computation.task.projectanalysis.measure.Measure.
 public class MaintainabilityMeasuresVisitor extends PathAwareVisitorAdapter<MaintainabilityMeasuresVisitor.Counter> {
   private final MeasureRepository measureRepository;
   private final RatingSettings ratingSettings;
-  private final RatingGrid ratingGrid;
 
   private final Metric nclocMetric;
   private final Metric developmentCostMetric;
@@ -64,7 +62,6 @@ public class MaintainabilityMeasuresVisitor extends PathAwareVisitorAdapter<Main
     super(CrawlerDepthLimit.FILE, POST_ORDER, CounterFactory.INSTANCE);
     this.measureRepository = measureRepository;
     this.ratingSettings = ratingSettings;
-    this.ratingGrid = ratingSettings.getRatingGrid();
 
     // Input metrics
     this.nclocMetric = metricRepository.getByKey(NCLOC_KEY);
@@ -134,7 +131,7 @@ public class MaintainabilityMeasuresVisitor extends PathAwareVisitorAdapter<Main
   }
 
   private void addMaintainabilityRatingMeasure(Component component, double density) {
-    Rating rating = ratingGrid.getRatingForDensity(density);
+    Rating rating = ratingSettings.getDebtRatingGrid().getRatingForDensity(density);
     measureRepository.add(component, maintainabilityRatingMetric, newMeasureBuilder().create(rating.getIndex(), rating.name()));
   }
 
@@ -142,7 +139,7 @@ public class MaintainabilityMeasuresVisitor extends PathAwareVisitorAdapter<Main
     long developmentCostValue = path.current().devCosts;
     Optional<Measure> effortMeasure = measureRepository.getRawMeasure(component, maintainabilityRemediationEffortMetric);
     long effort = effortMeasure.isPresent() ? effortMeasure.get().getLongValue() : 0L;
-    long upperGradeCost = ((Double) (ratingGrid.getGradeLowerBound(Rating.B) * developmentCostValue)).longValue();
+    long upperGradeCost = ((Double) (ratingSettings.getDebtRatingGrid().getGradeLowerBound(Rating.B) * developmentCostValue)).longValue();
     long effortToRatingA = upperGradeCost < effort ? (effort - upperGradeCost) : 0L;
     measureRepository.add(component, effortToMaintainabilityRatingAMetric, Measure.newMeasureBuilder().create(effortToRatingA));
   }
index 69aa35bf076b7fcff8fbc70a520ad07ea214903a..ff993ee893237fb82d7b030d55aa1e13fb739c2f 100644 (file)
@@ -42,6 +42,7 @@ import org.sonar.server.computation.task.projectanalysis.scm.ScmInfo;
 import org.sonar.server.computation.task.projectanalysis.scm.ScmInfoRepository;
 
 import static org.sonar.api.measures.CoreMetrics.NCLOC_DATA_KEY;
+import static org.sonar.api.measures.CoreMetrics.NEW_DEVELOPMENT_COST_KEY;
 import static org.sonar.api.measures.CoreMetrics.NEW_MAINTAINABILITY_RATING_KEY;
 import static org.sonar.api.measures.CoreMetrics.NEW_SQALE_DEBT_RATIO_KEY;
 import static org.sonar.api.measures.CoreMetrics.NEW_TECHNICAL_DEBT_KEY;
@@ -64,22 +65,21 @@ public class NewMaintainabilityMeasuresVisitor extends PathAwareVisitorAdapter<N
   private final MeasureRepository measureRepository;
   private final PeriodHolder periodHolder;
   private final RatingSettings ratingSettings;
-  private final RatingGrid ratingGrid;
 
   private final Metric newDebtMetric;
   private final Metric nclocDataMetric;
 
+  private final Metric newDevelopmentCostMetric;
   private final Metric newDebtRatioMetric;
   private final Metric newMaintainabilityRatingMetric;
 
   public NewMaintainabilityMeasuresVisitor(MetricRepository metricRepository, MeasureRepository measureRepository, ScmInfoRepository scmInfoRepository,
-                                           PeriodHolder periodHolder, RatingSettings ratingSettings) {
+    PeriodHolder periodHolder, RatingSettings ratingSettings) {
     super(CrawlerDepthLimit.FILE, POST_ORDER, CounterFactory.INSTANCE);
     this.measureRepository = measureRepository;
     this.scmInfoRepository = scmInfoRepository;
     this.periodHolder = periodHolder;
     this.ratingSettings = ratingSettings;
-    this.ratingGrid = ratingSettings.getRatingGrid();
 
     // computed by NewDebtAggregator which is executed by IntegrateIssuesVisitor
     this.newDebtMetric = metricRepository.getByKey(NEW_TECHNICAL_DEBT_KEY);
@@ -87,6 +87,7 @@ public class NewMaintainabilityMeasuresVisitor extends PathAwareVisitorAdapter<N
     this.nclocDataMetric = metricRepository.getByKey(NCLOC_DATA_KEY);
 
     // output metrics
+    this.newDevelopmentCostMetric = metricRepository.getByKey(NEW_DEVELOPMENT_COST_KEY);
     this.newDebtRatioMetric = metricRepository.getByKey(NEW_SQALE_DEBT_RATIO_KEY);
     this.newMaintainabilityRatingMetric = metricRepository.getByKey(NEW_MAINTAINABILITY_RATING_KEY);
   }
@@ -121,7 +122,9 @@ public class NewMaintainabilityMeasuresVisitor extends PathAwareVisitorAdapter<N
     }
     double density = computeDensity(path.current());
     double newDebtRatio = 100.0 * density;
-    double newMaintainability = ratingGrid.getRatingForDensity(density).getIndex();
+    double newMaintainability = ratingSettings.getDebtRatingGrid().getRatingForDensity(density).getIndex();
+    long newDevelopmentCost = path.current().getDevCost().getValue();
+    measureRepository.add(component, this.newDevelopmentCostMetric, newMeasureBuilder().setVariation(newDevelopmentCost).createNoValue());
     measureRepository.add(component, this.newDebtRatioMetric, newMeasureBuilder().setVariation(newDebtRatio).createNoValue());
     measureRepository.add(component, this.newMaintainabilityRatingMetric, newMeasureBuilder().setVariation(newMaintainability).createNoValue());
   }
index 34d26986865f30c6129902b5560be5341dd87116..b683fab8ceb0aaf7c15c1ae81ffec14448021900 100644 (file)
@@ -47,12 +47,12 @@ import static org.sonar.api.utils.DateUtils.truncateToSeconds;
 import static org.sonar.server.computation.task.projectanalysis.component.ComponentVisitor.Order.POST_ORDER;
 import static org.sonar.server.computation.task.projectanalysis.component.CrawlerDepthLimit.LEAVES;
 import static org.sonar.server.computation.task.projectanalysis.measure.Measure.newMeasureBuilder;
-import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating;
-import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.A;
-import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.B;
-import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.C;
-import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.D;
-import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.E;
+
+import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.A;
+import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.B;
+import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.C;
+import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.D;
+import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.E;
 
 /**
  * Compute following measures :
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/Rating.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/Rating.java
new file mode 100644 (file)
index 0000000..82e3793
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.computation.task.projectanalysis.qualitymodel;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+
+import static java.lang.String.format;
+import static java.util.Arrays.stream;
+import static org.sonar.api.rule.Severity.BLOCKER;
+import static org.sonar.api.rule.Severity.CRITICAL;
+import static org.sonar.api.rule.Severity.INFO;
+import static org.sonar.api.rule.Severity.MAJOR;
+import static org.sonar.api.rule.Severity.MINOR;
+
+public enum Rating {
+  E(5),
+  D(4),
+  C(3),
+  B(2),
+  A(1);
+
+  private final int index;
+
+  Rating(int index) {
+    this.index = index;
+  }
+
+  public int getIndex() {
+    return index;
+  }
+
+  public static Rating valueOf(int index) {
+    return stream(Rating.values())
+      .filter(r -> r.getIndex() == index)
+      .findFirst()
+      .orElseThrow(() -> new IllegalArgumentException(format("Unknown value '%s'", index)));
+  }
+
+  public static final Map<String, Rating> RATING_BY_SEVERITY = ImmutableMap.of(
+    BLOCKER, E,
+    CRITICAL, D,
+    MAJOR, C,
+    MINOR, B,
+    INFO, A);
+}
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/RatingGrid.java
deleted file mode 100644 (file)
index 0b33207..0000000
+++ /dev/null
@@ -1,85 +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.computation.task.projectanalysis.qualitymodel;
-
-import com.google.common.annotations.VisibleForTesting;
-import java.util.Arrays;
-import org.sonar.api.utils.MessageException;
-
-import static java.lang.String.format;
-import static java.util.Arrays.stream;
-
-public class RatingGrid {
-
-  private final double[] gridValues;
-
-  RatingGrid(double[] gridValues) {
-    this.gridValues = Arrays.copyOf(gridValues, gridValues.length);
-  }
-
-  Rating getRatingForDensity(double density) {
-    for (Rating rating : Rating.values()) {
-      double lowerBound = getGradeLowerBound(rating);
-      if (density >= lowerBound) {
-        return rating;
-      }
-    }
-    throw MessageException.of("The rating density value should be between 0 and " + Double.MAX_VALUE + " and got " + density);
-  }
-
-  double getGradeLowerBound(Rating rating) {
-    if (rating.getIndex() > 1) {
-      return gridValues[rating.getIndex() - 2];
-    }
-    return 0;
-  }
-
-  @VisibleForTesting
-  double[] getGridValues() {
-    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));
-    }
-  }
-
-}
index fa8669eddc4947f3f5ee7e5fb24b5e4ae7b02ecd..023875ff2b81b3b3f27b1f7cbb14c7034939a71d 100644 (file)
@@ -24,6 +24,7 @@ import java.util.Map;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
+import org.sonar.api.ce.ComputeEngineSide;
 import org.sonar.api.config.Configuration;
 import org.sonar.api.utils.MessageException;
 
@@ -33,45 +34,22 @@ import static org.sonar.api.CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS;
 import static org.sonar.api.CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS_LANGUAGE_KEY;
 import static org.sonar.api.CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS_MAN_DAYS_KEY;
 import static org.sonar.api.CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS_SIZE_METRIC_KEY;
-import static org.sonar.api.CoreProperties.RATING_GRID;
-import static org.sonar.api.CoreProperties.RATING_GRID_DEF_VALUES;
 
+@ComputeEngineSide
 public class RatingSettings {
 
-  private final Configuration config;
+  private final DebtRatingGrid ratingGrid;
+  private final long defaultDevCost;
   private final Map<String, LanguageSpecificConfiguration> languageSpecificConfigurationByLanguageKey;
 
   public RatingSettings(Configuration config) {
-    this.config = config;
-    this.languageSpecificConfigurationByLanguageKey = buildLanguageSpecificConfigurationByLanguageKey(config);
+    ratingGrid = new DebtRatingGrid(config);
+    defaultDevCost = initDefaultDevelopmentCost(config);
+    languageSpecificConfigurationByLanguageKey = initLanguageSpecificConfigurationByLanguageKey(config);
   }
 
-  private static Map<String, LanguageSpecificConfiguration> buildLanguageSpecificConfigurationByLanguageKey(Configuration config) {
-    ImmutableMap.Builder<String, LanguageSpecificConfiguration> builder = ImmutableMap.builder();
-    String[] languageConfigIndexes = config.getStringArray(LANGUAGE_SPECIFIC_PARAMETERS);
-    for (String languageConfigIndex : languageConfigIndexes) {
-      String languagePropertyKey = LANGUAGE_SPECIFIC_PARAMETERS + "." + languageConfigIndex + "." + LANGUAGE_SPECIFIC_PARAMETERS_LANGUAGE_KEY;
-      String languageKey = config.get(languagePropertyKey)
-        .orElseThrow(() -> MessageException.of("Technical debt configuration is corrupted. At least one language specific parameter has no Language key. " +
-          "Contact your administrator to update this configuration in the global administration section of SonarQube."));
-      builder.put(languageKey, LanguageSpecificConfiguration.create(config, languageConfigIndex));
-    }
-    return builder.build();
-  }
-
-  public RatingGrid getRatingGrid() {
-    try {
-      String[] ratingGrades = config.getStringArray(RATING_GRID);
-      double[] grid = new double[4];
-      for (int i = 0; i < 4; i++) {
-        grid[i] = Double.parseDouble(ratingGrades[i]);
-      }
-      return new RatingGrid(grid);
-    } catch (Exception e) {
-      throw new IllegalArgumentException("The rating grid is incorrect. Expected something similar to '"
-        + RATING_GRID_DEF_VALUES + "' and got '"
-        + config.get(RATING_GRID).get() + "'", e);
-    }
+  public DebtRatingGrid getDebtRatingGrid() {
+    return ratingGrid;
   }
 
   public long getDevCost(@Nullable String languageKey) {
@@ -86,10 +64,28 @@ public class RatingSettings {
       }
     }
 
-    return getDefaultDevelopmentCost();
+    return defaultDevCost;
   }
 
-  private long getDefaultDevelopmentCost() {
+  @CheckForNull
+  private LanguageSpecificConfiguration getSpecificParametersForLanguage(String languageKey) {
+    return languageSpecificConfigurationByLanguageKey.get(languageKey);
+  }
+
+  private static Map<String, LanguageSpecificConfiguration> initLanguageSpecificConfigurationByLanguageKey(Configuration config) {
+    ImmutableMap.Builder<String, LanguageSpecificConfiguration> builder = ImmutableMap.builder();
+    String[] languageConfigIndexes = config.getStringArray(LANGUAGE_SPECIFIC_PARAMETERS);
+    for (String languageConfigIndex : languageConfigIndexes) {
+      String languagePropertyKey = LANGUAGE_SPECIFIC_PARAMETERS + "." + languageConfigIndex + "." + LANGUAGE_SPECIFIC_PARAMETERS_LANGUAGE_KEY;
+      String languageKey = config.get(languagePropertyKey)
+        .orElseThrow(() -> MessageException.of("Technical debt configuration is corrupted. At least one language specific parameter has no Language key. " +
+          "Contact your administrator to update this configuration in the global administration section of SonarQube."));
+      builder.put(languageKey, LanguageSpecificConfiguration.create(config, languageConfigIndex));
+    }
+    return builder.build();
+  }
+
+  private static long initDefaultDevelopmentCost(Configuration config) {
     try {
       return Long.parseLong(config.get(DEVELOPMENT_COST).get());
     } catch (NumberFormatException e) {
@@ -98,11 +94,6 @@ public class RatingSettings {
     }
   }
 
-  @CheckForNull
-  private LanguageSpecificConfiguration getSpecificParametersForLanguage(String languageKey) {
-    return languageSpecificConfigurationByLanguageKey.get(languageKey);
-  }
-
   @Immutable
   private static class LanguageSpecificConfiguration {
     private final String language;
index 64c5ce3d2798a6f56da5985751333fde55cfb5e0..b9d55d835ff19e15a2a7b3cdaa0b5a3e9c1987fe 100644 (file)
@@ -21,35 +21,23 @@ package org.sonar.server.computation.task.projectanalysis.qualitymodel;
 
 import com.google.common.collect.ImmutableMap;
 import java.util.Map;
-import org.sonar.api.ce.measure.Issue;
 import org.sonar.api.measures.CoreMetrics;
 import org.sonar.server.computation.task.projectanalysis.component.Component;
 import org.sonar.server.computation.task.projectanalysis.component.PathAwareVisitorAdapter;
 import org.sonar.server.computation.task.projectanalysis.formula.counter.RatingValue;
 import org.sonar.server.computation.task.projectanalysis.issue.ComponentIssuesRepository;
-import org.sonar.server.computation.task.projectanalysis.measure.Measure;
 import org.sonar.server.computation.task.projectanalysis.measure.MeasureRepository;
 import org.sonar.server.computation.task.projectanalysis.metric.Metric;
 import org.sonar.server.computation.task.projectanalysis.metric.MetricRepository;
 
 import static org.sonar.api.measures.CoreMetrics.RELIABILITY_RATING_KEY;
 import static org.sonar.api.measures.CoreMetrics.SECURITY_RATING_KEY;
-import static org.sonar.api.rule.Severity.BLOCKER;
-import static org.sonar.api.rule.Severity.CRITICAL;
-import static org.sonar.api.rule.Severity.INFO;
-import static org.sonar.api.rule.Severity.MAJOR;
-import static org.sonar.api.rule.Severity.MINOR;
 import static org.sonar.api.rules.RuleType.BUG;
 import static org.sonar.api.rules.RuleType.VULNERABILITY;
 import static org.sonar.server.computation.task.projectanalysis.component.ComponentVisitor.Order.POST_ORDER;
 import static org.sonar.server.computation.task.projectanalysis.component.CrawlerDepthLimit.FILE;
 import static org.sonar.server.computation.task.projectanalysis.measure.Measure.newMeasureBuilder;
-import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating;
-import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.A;
-import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.B;
-import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.C;
-import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.D;
-import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.E;
+import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.RATING_BY_SEVERITY;
 
 /**
  * Compute following measures for projects and descendants:
@@ -58,20 +46,8 @@ import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rat
  */
 public class ReliabilityAndSecurityRatingMeasuresVisitor extends PathAwareVisitorAdapter<ReliabilityAndSecurityRatingMeasuresVisitor.Counter> {
 
-  private static final Map<String, Rating> RATING_BY_SEVERITY = ImmutableMap.of(
-    BLOCKER, E,
-    CRITICAL, D,
-    MAJOR, C,
-    MINOR, B,
-    INFO, A);
-
   private final MeasureRepository measureRepository;
   private final ComponentIssuesRepository componentIssuesRepository;
-
-  // Output metrics
-  private final Metric reliabilityRatingMetric;
-  private final Metric securityRatingMetric;
-
   private final Map<String, Metric> metricsByKey;
 
   public ReliabilityAndSecurityRatingMeasuresVisitor(MetricRepository metricRepository, MeasureRepository measureRepository, ComponentIssuesRepository componentIssuesRepository) {
@@ -80,8 +56,8 @@ public class ReliabilityAndSecurityRatingMeasuresVisitor extends PathAwareVisito
     this.componentIssuesRepository = componentIssuesRepository;
 
     // Output metrics
-    this.reliabilityRatingMetric = metricRepository.getByKey(RELIABILITY_RATING_KEY);
-    this.securityRatingMetric = metricRepository.getByKey(SECURITY_RATING_KEY);
+    Metric reliabilityRatingMetric = metricRepository.getByKey(RELIABILITY_RATING_KEY);
+    Metric securityRatingMetric = metricRepository.getByKey(SECURITY_RATING_KEY);
 
     this.metricsByKey = ImmutableMap.of(
       RELIABILITY_RATING_KEY, reliabilityRatingMetric,
@@ -94,13 +70,13 @@ public class ReliabilityAndSecurityRatingMeasuresVisitor extends PathAwareVisito
   }
 
   @Override
-  public void visitDirectory(Component directory, Path<Counter> path) {
-    computeAndSaveMeasures(directory, path);
+  public void visitModule(Component module, Path<Counter> path) {
+    computeAndSaveMeasures(module, path);
   }
 
   @Override
-  public void visitModule(Component module, Path<Counter> path) {
-    computeAndSaveMeasures(module, path);
+  public void visitDirectory(Component directory, Path<Counter> path) {
+    computeAndSaveMeasures(directory, path);
   }
 
   @Override
@@ -110,27 +86,27 @@ public class ReliabilityAndSecurityRatingMeasuresVisitor extends PathAwareVisito
 
   private void computeAndSaveMeasures(Component component, Path<Counter> path) {
     processIssues(component, path);
-    path.current().ratingValueByMetric.entrySet().forEach(
-      entry -> measureRepository.add(component, metricsByKey.get(entry.getKey()), createRatingMeasure(entry.getValue().getValue())));
-    addToParent(path);
+    path.current().ratingValueByMetric.forEach((key, value) -> {
+      Rating rating = value.getValue();
+      measureRepository.add(component, metricsByKey.get(key), newMeasureBuilder().create(rating.getIndex(), rating.name()));
+    });
+    if (!path.isRoot()) {
+      path.parent().add(path.current());
+    }
   }
 
   private void processIssues(Component component, Path<Counter> path) {
     componentIssuesRepository.getIssues(component)
       .stream()
       .filter(issue -> issue.resolution() == null)
-      .filter(issue -> issue.type().equals(BUG) || issue.type().equals(VULNERABILITY))
-      .forEach(path.current()::processIssue);
-  }
-
-  private static void addToParent(Path<Counter> path) {
-    if (!path.isRoot()) {
-      path.parent().add(path.current());
-    }
-  }
-
-  private static Measure createRatingMeasure(Rating rating) {
-    return newMeasureBuilder().create(rating.getIndex(), rating.name());
+      .forEach(issue -> {
+        Rating rating = RATING_BY_SEVERITY.get(issue.severity());
+        if (issue.type().equals(BUG)) {
+          path.current().ratingValueByMetric.get(RELIABILITY_RATING_KEY).increment(rating);
+        } else if (issue.type().equals(VULNERABILITY)) {
+          path.current().ratingValueByMetric.get(SECURITY_RATING_KEY).increment(rating);
+        }
+      });
   }
 
   static final class Counter {
@@ -143,17 +119,9 @@ public class ReliabilityAndSecurityRatingMeasuresVisitor extends PathAwareVisito
     }
 
     void add(Counter otherCounter) {
-      ratingValueByMetric.entrySet().forEach(e -> e.getValue().increment(otherCounter.ratingValueByMetric.get(e.getKey())));
+      ratingValueByMetric.forEach((key, value) -> value.increment(otherCounter.ratingValueByMetric.get(key)));
     }
 
-    void processIssue(Issue issue) {
-      Rating rating = RATING_BY_SEVERITY.get(issue.severity());
-      if (issue.type().equals(BUG)) {
-        ratingValueByMetric.get(RELIABILITY_RATING_KEY).increment(rating);
-      } else if (issue.type().equals(VULNERABILITY)) {
-        ratingValueByMetric.get(SECURITY_RATING_KEY).increment(rating);
-      }
-    }
   }
 
   private static final class CounterFactory extends PathAwareVisitorAdapter.SimpleStackElementFactory<ReliabilityAndSecurityRatingMeasuresVisitor.Counter> {
index 6696baad527f932964270d73307c824a2334e545..7838e9091c25e654f1284aef2fc2016963015a56 100644 (file)
@@ -59,15 +59,11 @@ public class LoadQualityGateStep implements ComputationStep {
       qualityGate = getProjectQualityGate();
       if (!qualityGate.isPresent()) {
         // No QG defined for the project, let's retrieve the QG on the organization
-        qualityGate = getOrganizationDefaultQualityGate();
+        qualityGate = Optional.of(getOrganizationDefaultQualityGate());
       }
     }
 
-    if (qualityGate.isPresent()) {
-      qualityGateHolder.setQualityGate(qualityGate.get());
-    } else {
-      qualityGateHolder.setNoQualityGate();
-    }
+    qualityGateHolder.setQualityGate(qualityGate.orElseThrow(() -> new IllegalStateException("Quality gate not present")));
   }
 
   private Optional<QualityGate> getShortLivingBranchQualityGate() {
@@ -100,7 +96,7 @@ public class LoadQualityGateStep implements ComputationStep {
     }
   }
 
-  private Optional<QualityGate> getOrganizationDefaultQualityGate() {
+  private QualityGate getOrganizationDefaultQualityGate() {
     return qualityGateService.findDefaultQualityGate(analysisMetadataHolder.getOrganization());
   }
 
index cd78e6a0e21d8eed3b73366002886f38ae0f18f8..8fce93dbe391e1f07627258311df63de91eaa690 100644 (file)
@@ -20,7 +20,6 @@
 package org.sonar.server.computation.task.projectanalysis.step;
 
 import com.google.common.base.Function;
-import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Ordering;
@@ -28,6 +27,7 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -177,7 +177,7 @@ public class QualityGateMeasuresStep implements ComputationStep {
     boolean ignoredConditions = false;
     for (Map.Entry<Metric, Collection<Condition>> entry : conditionsPerMetric.asMap().entrySet()) {
       Metric metric = entry.getKey();
-      Optional<Measure> measure = measureRepository.getRawMeasure(project, metric);
+      com.google.common.base.Optional<Measure> measure = measureRepository.getRawMeasure(project, metric);
       if (!measure.isPresent()) {
         continue;
       }
index 16eea512659c7b3648f23f202b1c7ae08f264d37..576df71153e07cfd4aa57a64a3506f49bada5e24 100644 (file)
@@ -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);
index 40e454e6de33197e4433894ff1473e62dc1d1c1c..e236161d1dabdc1ae147b0dad8b68a8135c0174a 100644 (file)
@@ -40,7 +40,8 @@ public interface ProjectIndexer extends ResilientIndexer {
     PROJECT_DELETION,
     PROJECT_KEY_UPDATE,
     PROJECT_TAGS_UPDATE,
-    PERMISSION_CHANGE
+    PERMISSION_CHANGE,
+    MEASURE_CHANGE
   }
 
   /**
index 4df063c29d939b0bfa7b87b51b4ed1006df21ab0..ac81874ef5042f37f0fc4fbfd538050a1765d440 100644 (file)
@@ -60,6 +60,11 @@ public abstract class AbstractChangeTagsAction extends Action {
 
   protected abstract Collection<String> getTagsToSet(Context context, Collection<String> tagsFromParams);
 
+  @Override
+  public boolean shouldRefreshMeasures() {
+    return false;
+  }
+
   private Set<String> parseTags(Map<String, Object> properties) {
     Set<String> result = new HashSet<>();
     String tagsString = (String) properties.get(TAGS_PARAMETER);
index 5177042d609cb83e564a69df91f4fd03bc42e7a7..24f00e1895e734e5c3ba81937152f6fb5589b641 100644 (file)
@@ -75,6 +75,8 @@ public abstract class Action {
 
   public abstract boolean execute(Map<String, Object> properties, Context context);
 
+  public abstract boolean shouldRefreshMeasures();
+
   public interface Context {
     DefaultIssue issue();
 
index 7c522fc3286f513693232a47f1d8056ef52a2ba6..90aa92e568b02e2dc09cad822d914797c940def6 100644 (file)
@@ -89,6 +89,11 @@ public class AssignAction extends Action {
     return isAssigneeMemberOfIssueOrganization(assignee, properties, context) && issueFieldsSetter.assign(context.issue(), assignee, context.issueChangeContext());
   }
 
+  @Override
+  public boolean shouldRefreshMeasures() {
+    return false;
+  }
+
   private static boolean isAssigneeMemberOfIssueOrganization(@Nullable UserDto assignee, Map<String, Object> properties, Context context) {
     return assignee == null || ((Set<String>) properties.get(ASSIGNEE_ORGANIZATIONS)).contains(context.project().getOrganizationUuid());
   }
index 45ceb02870140dff4c6e5b963eae341abc9b53bd..bab386354c5e5366ab85089b52de871cde41c519 100644 (file)
@@ -51,6 +51,11 @@ public class CommentAction extends Action {
     return true;
   }
 
+  @Override
+  public boolean shouldRefreshMeasures() {
+    return false;
+  }
+
   private static String comment(Map<String, Object> properties) {
     String param = (String) properties.get(COMMENT_PROPERTY);
     if (Strings.isNullOrEmpty(param)) {
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 (file)
index 0000000..b0d28c6
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.issue;
+
+import java.util.Collection;
+import java.util.List;
+import org.sonar.api.server.ServerSide;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+
+@ServerSide
+public interface IssueChangePostProcessor {
+
+  /**
+   * Refresh measures, quality gate status and send webhooks
+   *
+   * @param components the components of changed issues
+   */
+  void process(DbSession dbSession, List<DefaultIssue> changedIssues, Collection<ComponentDto> components);
+
+}
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 (file)
index 0000000..c19e9ba
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.issue;
+
+import java.util.Collection;
+import java.util.List;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.server.measure.live.LiveMeasureComputer;
+import org.sonar.server.qualitygate.changeevent.QGChangeEvent;
+import org.sonar.server.qualitygate.changeevent.QGChangeEventListeners;
+
+public class IssueChangePostProcessorImpl implements IssueChangePostProcessor {
+
+  private final LiveMeasureComputer liveMeasureComputer;
+  private final QGChangeEventListeners qualityGateListeners;
+
+  public IssueChangePostProcessorImpl(LiveMeasureComputer liveMeasureComputer, QGChangeEventListeners qualityGateListeners) {
+    this.liveMeasureComputer = liveMeasureComputer;
+    this.qualityGateListeners = qualityGateListeners;
+  }
+
+  @Override
+  public void process(DbSession dbSession, List<DefaultIssue> changedIssues, Collection<ComponentDto> components) {
+    List<QGChangeEvent> gateChangeEvents = liveMeasureComputer.refresh(dbSession, components);
+    qualityGateListeners.broadcastOnIssueChange(changedIssues, gateChangeEvents);
+  }
+}
index 8f3aa25c0b75269523402d2003c349a49d4b288e..8dac9d11f000cffc17a21aac03d0920f80a1af8d 100644 (file)
  */
 package org.sonar.server.issue;
 
+import java.util.List;
 import java.util.Optional;
 import javax.annotation.Nullable;
 import org.sonar.api.rule.RuleKey;
 import org.sonar.api.rule.RuleStatus;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.core.issue.IssueChangeContext;
+import org.sonar.core.util.stream.MoreCollectors;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.component.ComponentDto;
@@ -42,38 +44,48 @@ public class IssueUpdater {
   private final DbClient dbClient;
   private final IssueStorage issueStorage;
   private final NotificationManager notificationService;
+  private final IssueChangePostProcessor issueChangePostProcessor;
 
-  public IssueUpdater(DbClient dbClient, IssueStorage issueStorage, NotificationManager notificationService) {
+  public IssueUpdater(DbClient dbClient, IssueStorage issueStorage, NotificationManager notificationService,
+    IssueChangePostProcessor issueChangePostProcessor) {
     this.dbClient = dbClient;
     this.issueStorage = issueStorage;
     this.notificationService = notificationService;
+    this.issueChangePostProcessor = issueChangePostProcessor;
   }
 
   /**
    * Same as {@link #saveIssue(DbSession, DefaultIssue, IssueChangeContext, String)} but populates the specified
    * {@link SearchResponseData} with the DTOs (rule and components) retrieved from DB to save the issue.
    */
-  public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession session, DefaultIssue issue, IssueChangeContext context, @Nullable String comment) {
-    Optional<RuleDefinitionDto> rule = getRuleByKey(session, issue.getRuleKey());
-    ComponentDto project = dbClient.componentDao().selectOrFailByUuid(session, issue.projectUuid());
-    ComponentDto component = dbClient.componentDao().selectOrFailByUuid(session, issue.componentUuid());
-    IssueDto issueDto = saveIssue(session, issue, context, comment, rule, project, component);
+  public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession dbSession, DefaultIssue issue, IssueChangeContext context,
+    @Nullable String comment, boolean refreshMeasures) {
+    Optional<RuleDefinitionDto> rule = getRuleByKey(dbSession, issue.getRuleKey());
+    ComponentDto project = dbClient.componentDao().selectOrFailByUuid(dbSession, issue.projectUuid());
+    ComponentDto component = dbClient.componentDao().selectOrFailByUuid(dbSession, issue.componentUuid());
+    IssueDto issueDto = doSaveIssue(dbSession, issue, context, comment, rule, project, component);
+
+    SearchResponseData result = new SearchResponseData(issueDto);
+    rule.ifPresent(r -> result.setRules(singletonList(r)));
+    result.addComponents(singleton(project));
+    result.addComponents(singleton(component));
+
+    if (refreshMeasures) {
+      List<DefaultIssue> changedIssues = result.getIssues().stream().map(IssueDto::toDefaultIssue).collect(MoreCollectors.toList(result.getIssues().size()));
+      issueChangePostProcessor.process(dbSession, changedIssues, singleton(component));
+    }
 
-    SearchResponseData preloadedSearchResponseData = new SearchResponseData(issueDto);
-    rule.ifPresent(r -> preloadedSearchResponseData.setRules(singletonList(r)));
-    preloadedSearchResponseData.addComponents(singleton(project));
-    preloadedSearchResponseData.addComponents(singleton(component));
-    return preloadedSearchResponseData;
+    return result;
   }
 
   public IssueDto saveIssue(DbSession session, DefaultIssue issue, IssueChangeContext context, @Nullable String comment) {
     Optional<RuleDefinitionDto> rule = getRuleByKey(session, issue.getRuleKey());
     ComponentDto project = dbClient.componentDao().selectOrFailByUuid(session, issue.projectUuid());
     ComponentDto component = dbClient.componentDao().selectOrFailByUuid(session, issue.componentUuid());
-    return saveIssue(session, issue, context, comment, rule, project, component);
+    return doSaveIssue(session, issue, context, comment, rule, project, component);
   }
 
-  private IssueDto saveIssue(DbSession session, DefaultIssue issue, IssueChangeContext context, @Nullable String comment,
+  private IssueDto doSaveIssue(DbSession session, DefaultIssue issue, IssueChangeContext context, @Nullable String comment,
     Optional<RuleDefinitionDto> rule, ComponentDto project, ComponentDto component) {
     IssueDto issueDto = issueStorage.save(session, issue);
     notificationService.scheduleForSending(new IssueChangeNotification()
@@ -87,7 +99,7 @@ public class IssueUpdater {
   }
 
   private Optional<RuleDefinitionDto> getRuleByKey(DbSession session, RuleKey ruleKey) {
-    Optional<RuleDefinitionDto> rule = Optional.ofNullable(dbClient.ruleDao().selectDefinitionByKey(session, ruleKey).orElse(null));
+    Optional<RuleDefinitionDto> rule = dbClient.ruleDao().selectDefinitionByKey(session, ruleKey);
     return (rule.isPresent() && rule.get().getStatus() != RuleStatus.REMOVED) ? rule : Optional.empty();
   }
 
index d1909e9c1ec5ddf84ae109c568bad088406cba5f..3b905d1011fa51274da5d2e923df51cad2964345 100644 (file)
@@ -61,6 +61,11 @@ public class SetSeverityAction extends Action {
     return issueUpdater.setManualSeverity(context.issue(), verifySeverityParameter(properties), context.issueChangeContext());
   }
 
+  @Override
+  public boolean shouldRefreshMeasures() {
+    return true;
+  }
+
   private static String verifySeverityParameter(Map<String, Object> properties) {
     String param = (String) properties.get(SEVERITY_PARAMETER);
     checkArgument(!isNullOrEmpty(param), "Missing parameter : '%s'", SEVERITY_PARAMETER);
index 26909a4f8670233813e1f9d4372b2b13baa64fe7..193fe499247e65b188c5da1a1a4fe7be6560bb45 100644 (file)
@@ -61,6 +61,11 @@ public class SetTypeAction extends Action {
     return issueUpdater.setType(context.issue(), RuleType.valueOf(type), context.issueChangeContext());
   }
 
+  @Override
+  public boolean shouldRefreshMeasures() {
+    return true;
+  }
+
   private static String verifyTypeParameter(Map<String, Object> properties) {
     String type = (String) properties.get(TYPE_PARAMETER);
     checkArgument(!isNullOrEmpty(type), "Missing parameter : '%s'", TYPE_PARAMETER);
index 23c44754b454ff73df671c1f34004d53f13e2f00..444b105017b644b92bde6f6f2b1995e41010a16d 100644 (file)
@@ -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()
index 9b430a5f25e65d470d74b408cfc9e627d0cc15aa..308afd0df578eeda48430eb95e8c644d1c110a32 100644 (file)
@@ -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:
index 4993dca94e13b67ae5e429bf3ac3ad0091fb12bf..d501ed054264c78fb14b128b58e281a89c1cde50 100644 (file)
@@ -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);
     }
   }
index a65810ac2a8ae09bb2148187429727a35701f770..8cd574cca6397a7f20d6578261d06fbed3e1334c 100644 (file)
@@ -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);
     }
index 1edfaf4ab3e19b9ddc7004f1418f75f6452ffd95..11ed562656185217fa24595306c4065cfb9ef12d 100644 (file)
@@ -28,9 +28,8 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import java.util.function.Consumer;
-import java.util.function.Function;
 import java.util.function.Predicate;
-import java.util.stream.Stream;
+import java.util.stream.Collectors;
 import org.sonar.api.issue.DefaultTransitions;
 import org.sonar.api.rule.RuleKey;
 import org.sonar.api.rule.Severity;
@@ -54,20 +53,15 @@ import org.sonar.db.rule.RuleDefinitionDto;
 import org.sonar.server.issue.Action;
 import org.sonar.server.issue.AddTagsAction;
 import org.sonar.server.issue.AssignAction;
+import org.sonar.server.issue.IssueChangePostProcessor;
 import org.sonar.server.issue.IssueStorage;
 import org.sonar.server.issue.RemoveTagsAction;
-import org.sonar.server.issue.SetTypeAction;
-import org.sonar.server.issue.TransitionAction;
 import org.sonar.server.issue.notification.IssueChangeNotification;
 import org.sonar.server.notification.NotificationManager;
-import org.sonar.server.qualitygate.changeevent.QGChangeEvent;
-import org.sonar.server.qualitygate.changeevent.QGChangeEventFactory;
-import org.sonar.server.qualitygate.changeevent.QGChangeEventListeners;
 import org.sonar.server.user.UserSession;
 import org.sonarqube.ws.Issues;
 
 import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.collect.ImmutableList.copyOf;
 import static com.google.common.collect.ImmutableMap.of;
 import static java.lang.String.format;
 import static java.util.function.Function.identity;
@@ -111,20 +105,18 @@ public class BulkChangeAction implements IssuesWsAction {
   private final IssueStorage issueStorage;
   private final NotificationManager notificationService;
   private final List<Action> actions;
-  private final QGChangeEventFactory qgChangeEventFactory;
-  private final QGChangeEventListeners qgChangeEventListeners;
+  private final IssueChangePostProcessor issueChangePostProcessor;
 
   public BulkChangeAction(System2 system2, UserSession userSession, DbClient dbClient, IssueStorage issueStorage,
     NotificationManager notificationService, List<Action> actions,
-    QGChangeEventFactory qgChangeEventFactory, QGChangeEventListeners qgChangeEventListeners) {
+    IssueChangePostProcessor issueChangePostProcessor) {
     this.system2 = system2;
     this.userSession = userSession;
     this.dbClient = dbClient;
     this.issueStorage = issueStorage;
     this.notificationService = notificationService;
     this.actions = actions;
-    this.qgChangeEventFactory = qgChangeEventFactory;
-    this.qgChangeEventListeners = qgChangeEventListeners;
+    this.issueChangePostProcessor = issueChangePostProcessor;
   }
 
   @Override
@@ -187,53 +179,41 @@ public class BulkChangeAction implements IssuesWsAction {
   public void handle(Request request, Response response) throws Exception {
     userSession.checkLoggedIn();
     try (DbSession dbSession = dbClient.openSession(false)) {
-      Issues.BulkChangeWsResponse wsResponse = Stream.of(request)
-        .map(loadData(dbSession))
-        .map(executeBulkChange())
-        .map(toWsResponse())
-        .collect(MoreCollectors.toOneElement());
-      writeProtobuf(wsResponse, request, response);
+      BulkChangeResult result = executeBulkChange(dbSession, request);
+      writeProtobuf(toWsResponse(result), request, response);
     }
   }
 
-  private Function<Request, BulkChangeData> loadData(DbSession dbSession) {
-    return request -> new BulkChangeData(dbSession, request);
-  }
+  private BulkChangeResult executeBulkChange(DbSession dbSession, Request request) {
+    BulkChangeData bulkChangeData = new BulkChangeData(dbSession, request);
+    BulkChangeResult result = new BulkChangeResult(bulkChangeData.issues.size());
+    IssueChangeContext issueChangeContext = IssueChangeContext.createUser(new Date(system2.now()), userSession.getLogin());
 
-  private Function<BulkChangeData, BulkChangeResult> executeBulkChange() {
-    return bulkChangeData -> {
-      BulkChangeResult result = new BulkChangeResult(bulkChangeData.issues.size());
-      IssueChangeContext issueChangeContext = IssueChangeContext.createUser(new Date(system2.now()), userSession.getLogin());
+    List<DefaultIssue> items = bulkChangeData.issues.stream()
+      .filter(bulkChange(issueChangeContext, bulkChangeData, result))
+      .collect(MoreCollectors.toList());
+    issueStorage.save(items);
 
-      List<DefaultIssue> items = bulkChangeData.issues.stream()
-        .filter(bulkChange(issueChangeContext, bulkChangeData, result))
-        .collect(MoreCollectors.toList());
-      issueStorage.save(items);
-      items.forEach(sendNotification(issueChangeContext, bulkChangeData));
-      buildWebhookIssueChange(bulkChangeData.propertiesByActions)
-        .ifPresent(issueChange -> {
-          QGChangeEventFactory.IssueChangeData issueChangeData = new QGChangeEventFactory.IssueChangeData(
-            bulkChangeData.issues.stream().filter(i -> result.success.contains(i.key())).collect(MoreCollectors.toList()),
-            copyOf(bulkChangeData.componentsByUuid.values()));
-          List<QGChangeEvent> qgChangeEvents = qgChangeEventFactory.from(issueChangeData, issueChange, issueChangeContext);
-          qgChangeEventListeners.broadcastOnIssueChange(issueChangeData, qgChangeEvents);
-        });
-      return result;
-    };
+    refreshLiveMeasures(dbSession, bulkChangeData, result);
+
+    items.forEach(sendNotification(issueChangeContext, bulkChangeData));
+
+    return result;
   }
 
-  private static Optional<QGChangeEventFactory.IssueChange> buildWebhookIssueChange(Map<String, Map<String, Object>> propertiesByActions) {
-    RuleType ruleType = Optional.ofNullable(propertiesByActions.get(SetTypeAction.SET_TYPE_KEY))
-      .map(t -> (String) t.get(SetTypeAction.TYPE_PARAMETER))
-      .map(RuleType::valueOf)
-      .orElse(null);
-    String transitionKey = Optional.ofNullable(propertiesByActions.get(TransitionAction.DO_TRANSITION_KEY))
-      .map(t -> (String) t.get(TransitionAction.TRANSITION_PARAMETER))
-      .orElse(null);
-    if (ruleType == null && transitionKey == null) {
-      return Optional.empty();
+  private void refreshLiveMeasures(DbSession dbSession, BulkChangeData data, BulkChangeResult result) {
+    if (!data.shouldRefreshMeasures()) {
+      return;
     }
-    return Optional.of(new QGChangeEventFactory.IssueChange(ruleType, transitionKey));
+    Set<String> touchedComponentUuids = result.success.stream()
+      .map(DefaultIssue::componentUuid)
+      .collect(Collectors.toSet());
+    List<ComponentDto> touchedComponents = touchedComponentUuids.stream()
+      .map(data.componentsByUuid::get)
+      .collect(MoreCollectors.toList(touchedComponentUuids.size()));
+
+    List<DefaultIssue> changedIssues = data.issues.stream().filter(result.success::contains).collect(MoreCollectors.toList());
+    issueChangePostProcessor.process(dbSession, changedIssues, touchedComponents);
   }
 
   private static Predicate<DefaultIssue> bulkChange(IssueChangeContext issueChangeContext, BulkChangeData bulkChangeData, BulkChangeResult result) {
@@ -241,7 +221,7 @@ public class BulkChangeAction implements IssuesWsAction {
       ActionContext actionContext = new ActionContext(issue, issueChangeContext, bulkChangeData.projectsByUuid.get(issue.projectUuid()));
       bulkChangeData.getActionsWithoutComment().forEach(applyAction(actionContext, bulkChangeData, result));
       addCommentIfNeeded(actionContext, bulkChangeData);
-      return result.success.contains(issue.key());
+      return result.success.contains(issue);
     };
   }
 
@@ -277,12 +257,12 @@ public class BulkChangeAction implements IssuesWsAction {
     };
   }
 
-  private static Function<BulkChangeResult, Issues.BulkChangeWsResponse> toWsResponse() {
-    return bulkChangeResult -> Issues.BulkChangeWsResponse.newBuilder()
-      .setTotal(bulkChangeResult.getTotal())
-      .setSuccess(bulkChangeResult.getSuccess())
-      .setIgnored((long) bulkChangeResult.getTotal() - (bulkChangeResult.getSuccess() + bulkChangeResult.getFailures()))
-      .setFailures(bulkChangeResult.getFailures())
+  private static Issues.BulkChangeWsResponse toWsResponse(BulkChangeResult result) {
+    return Issues.BulkChangeWsResponse.newBuilder()
+      .setTotal(result.countTotal())
+      .setSuccess(result.countSuccess())
+      .setIgnored((long) result.countTotal() - (result.countSuccess() + result.countFailures()))
+      .setFailures(result.countFailures())
       .build();
   }
 
@@ -391,11 +371,15 @@ public class BulkChangeAction implements IssuesWsAction {
       long actionsDefined = actions.stream().filter(action -> !action.equals(COMMENT_KEY)).count();
       checkArgument(actionsDefined > 0, "At least one action must be provided");
     }
+
+    private boolean shouldRefreshMeasures() {
+      return availableActions.stream().anyMatch(Action::shouldRefreshMeasures);
+    }
   }
 
   private static class BulkChangeResult {
     private final int total;
-    private Set<String> success = new HashSet<>();
+    private final Set<DefaultIssue> success = new HashSet<>();
     private int failures = 0;
 
     BulkChangeResult(int total) {
@@ -403,22 +387,22 @@ public class BulkChangeAction implements IssuesWsAction {
     }
 
     void increaseSuccess(DefaultIssue issue) {
-      this.success.add(issue.key());
+      this.success.add(issue);
     }
 
     void increaseFailure() {
       this.failures++;
     }
 
-    public int getTotal() {
+    int countTotal() {
       return total;
     }
 
-    public int getSuccess() {
+    int countSuccess() {
       return success.size();
     }
 
-    public int getFailures() {
+    int countFailures() {
       return failures;
     }
   }
index ba9b4a0c5410ad4e8b11383dde6d45c96b88ac52..501d7f03fa75d6b573e5744e137ba12a65cb3863 100644 (file)
@@ -21,7 +21,6 @@ package org.sonar.server.issue.ws;
 
 import com.google.common.io.Resources;
 import java.util.Date;
-import java.util.List;
 import org.sonar.api.issue.DefaultTransitions;
 import org.sonar.api.server.ws.Change;
 import org.sonar.api.server.ws.Request;
@@ -37,13 +36,8 @@ import org.sonar.db.issue.IssueDto;
 import org.sonar.server.issue.IssueFinder;
 import org.sonar.server.issue.IssueUpdater;
 import org.sonar.server.issue.TransitionService;
-import org.sonar.server.qualitygate.changeevent.QGChangeEvent;
-import org.sonar.server.qualitygate.changeevent.QGChangeEventFactory;
-import org.sonar.server.qualitygate.changeevent.QGChangeEventListeners;
 import org.sonar.server.user.UserSession;
 
-import static com.google.common.collect.ImmutableList.copyOf;
-import static org.sonar.core.util.stream.MoreCollectors.toList;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_DO_TRANSITION;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUE;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_TRANSITION;
@@ -57,11 +51,9 @@ public class DoTransitionAction implements IssuesWsAction {
   private final TransitionService transitionService;
   private final OperationResponseWriter responseWriter;
   private final System2 system2;
-  private final QGChangeEventFactory qgChangeEventFactory;
-  private final QGChangeEventListeners qgChangeEventListeners;
 
   public DoTransitionAction(DbClient dbClient, UserSession userSession, IssueFinder issueFinder, IssueUpdater issueUpdater, TransitionService transitionService,
-    OperationResponseWriter responseWriter, System2 system2, QGChangeEventFactory qgChangeEventFactory, QGChangeEventListeners qgChangeEventListeners) {
+    OperationResponseWriter responseWriter, System2 system2) {
     this.dbClient = dbClient;
     this.userSession = userSession;
     this.issueFinder = issueFinder;
@@ -69,8 +61,6 @@ public class DoTransitionAction implements IssuesWsAction {
     this.transitionService = transitionService;
     this.responseWriter = responseWriter;
     this.system2 = system2;
-    this.qgChangeEventFactory = qgChangeEventFactory;
-    this.qgChangeEventListeners = qgChangeEventListeners;
   }
 
   @Override
@@ -112,13 +102,7 @@ public class DoTransitionAction implements IssuesWsAction {
     IssueChangeContext context = IssueChangeContext.createUser(new Date(system2.now()), userSession.getLogin());
     transitionService.checkTransitionPermission(transitionKey, defaultIssue);
     if (transitionService.doTransition(defaultIssue, context, transitionKey)) {
-      SearchResponseData searchResponseData = issueUpdater.saveIssueAndPreloadSearchResponseData(session, defaultIssue, context, null);
-      QGChangeEventFactory.IssueChangeData issueChangeData = new QGChangeEventFactory.IssueChangeData(
-        searchResponseData.getIssues().stream().map(IssueDto::toDefaultIssue).collect(toList(searchResponseData.getIssues().size())),
-        copyOf(searchResponseData.getComponents()));
-      List<QGChangeEvent> qgChangeEvents = qgChangeEventFactory.from(issueChangeData, new QGChangeEventFactory.IssueChange(transitionKey), context);
-      qgChangeEventListeners.broadcastOnIssueChange(issueChangeData, qgChangeEvents);
-      return searchResponseData;
+      return issueUpdater.saveIssueAndPreloadSearchResponseData(session, defaultIssue, context, null, true);
     }
     return new SearchResponseData(issueDto);
   }
index 7d4e1985a9617e6fa7d8d47969341d62053744a8..a434b524eed2ea53c4efceef95f40deaa4b61410 100644 (file)
@@ -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);
   }
index d7407b48d441aea1299f81b381e1ea1b58aa2043..c1cd538704b04608ae89fbdb88f773f1b0fc3a54 100644 (file)
@@ -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);
   }
index 778c03b681be761001ac51e15b3b973fe8dc9e05..004e916a0129442ed37eb1f7de39e963c9227ff6 100644 (file)
@@ -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);
     }
index 4239996386e0709e3ce9736f2b61f140e45f1962..d22d5fa26e03362ba20c0cb362fa8c4eb587ef45 100644 (file)
@@ -21,7 +21,6 @@ package org.sonar.server.issue.ws;
 
 import com.google.common.io.Resources;
 import java.util.Date;
-import java.util.List;
 import org.sonar.api.rules.RuleType;
 import org.sonar.api.server.ws.Change;
 import org.sonar.api.server.ws.Request;
@@ -31,19 +30,14 @@ import org.sonar.api.utils.System2;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.core.issue.IssueChangeContext;
 import org.sonar.core.util.Uuids;
-import org.sonar.core.util.stream.MoreCollectors;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.issue.IssueDto;
 import org.sonar.server.issue.IssueFieldsSetter;
 import org.sonar.server.issue.IssueFinder;
 import org.sonar.server.issue.IssueUpdater;
-import org.sonar.server.qualitygate.changeevent.QGChangeEvent;
-import org.sonar.server.qualitygate.changeevent.QGChangeEventFactory;
-import org.sonar.server.qualitygate.changeevent.QGChangeEventListeners;
 import org.sonar.server.user.UserSession;
 
-import static com.google.common.collect.ImmutableList.copyOf;
 import static org.sonar.api.web.UserRole.ISSUE_ADMIN;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_SET_TYPE;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUE;
@@ -58,12 +52,9 @@ public class SetTypeAction implements IssuesWsAction {
   private final IssueUpdater issueUpdater;
   private final OperationResponseWriter responseWriter;
   private final System2 system2;
-  private final QGChangeEventFactory qgChangeEventFactory;
-  private final QGChangeEventListeners qgChangeEventListeners;
 
   public SetTypeAction(UserSession userSession, DbClient dbClient, IssueFinder issueFinder, IssueFieldsSetter issueFieldsSetter, IssueUpdater issueUpdater,
-    OperationResponseWriter responseWriter, System2 system2,
-    QGChangeEventFactory qgChangeEventFactory, QGChangeEventListeners qgChangeEventListeners) {
+    OperationResponseWriter responseWriter, System2 system2) {
     this.userSession = userSession;
     this.dbClient = dbClient;
     this.issueFinder = issueFinder;
@@ -71,8 +62,6 @@ public class SetTypeAction implements IssuesWsAction {
     this.issueUpdater = issueUpdater;
     this.responseWriter = responseWriter;
     this.system2 = system2;
-    this.qgChangeEventFactory = qgChangeEventFactory;
-    this.qgChangeEventListeners = qgChangeEventListeners;
   }
 
   @Override
@@ -121,13 +110,7 @@ public class SetTypeAction implements IssuesWsAction {
 
     IssueChangeContext context = IssueChangeContext.createUser(new Date(system2.now()), userSession.getLogin());
     if (issueFieldsSetter.setType(issue, ruleType, context)) {
-      SearchResponseData searchResponseData = issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, null);
-      QGChangeEventFactory.IssueChangeData issueChangeData = new QGChangeEventFactory.IssueChangeData(
-        searchResponseData.getIssues().stream().map(IssueDto::toDefaultIssue).collect(MoreCollectors.toList(searchResponseData.getIssues().size())),
-        copyOf(searchResponseData.getComponents()));
-      List<QGChangeEvent> qgChangeEvents = qgChangeEventFactory.from(issueChangeData, new QGChangeEventFactory.IssueChange(ruleType), context);
-      qgChangeEventListeners.broadcastOnIssueChange(issueChangeData, qgChangeEvents);
-      return searchResponseData;
+      return issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, null, true);
     }
     return new SearchResponseData(issueDto);
   }
index 7c4651cc453ddf523d833477509a9c1a19c0efd1..bac783b89e3babe5cecd7a32fa245148c4a40d39 100644 (file)
@@ -78,8 +78,8 @@ public class ProjectMeasuresIndexer implements ProjectIndexer, NeedAuthorization
   }
 
   @Override
-  public void indexOnAnalysis(String branchUuid) {
-    doIndex(Size.REGULAR, branchUuid);
+  public void indexOnAnalysis(String projectUuid) {
+    doIndex(Size.REGULAR, projectUuid);
   }
 
   @Override
@@ -88,7 +88,7 @@ public class ProjectMeasuresIndexer implements ProjectIndexer, NeedAuthorization
       case PERMISSION_CHANGE:
         // nothing to do, permissions are not used in type projectmeasures/projectmeasure
         return Collections.emptyList();
-
+      case MEASURE_CHANGE:
       case PROJECT_KEY_UPDATE:
         // project must be re-indexed because key is used in this index
       case PROJECT_CREATION:
@@ -106,6 +106,17 @@ public class ProjectMeasuresIndexer implements ProjectIndexer, NeedAuthorization
     }
   }
 
+  public IndexingResult commitAndIndex(DbSession dbSession, Collection<String> projectUuids) {
+    List<EsQueueDto> items = projectUuids.stream()
+      .map(projectUuid -> EsQueueDto.create(INDEX_TYPE_PROJECT_MEASURES.format(), projectUuid, null, projectUuid))
+      .collect(MoreCollectors.toArrayList(projectUuids.size()));
+    dbClient.esQueueDao().insert(dbSession, items);
+
+    dbSession.commit();
+
+    return index(dbSession, items);
+  }
+
   @Override
   public IndexingResult index(DbSession dbSession, Collection<EsQueueDto> items) {
     if (items.isEmpty()) {
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 (file)
index 0000000..09e1e57
--- /dev/null
@@ -0,0 +1,153 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.measure.live;
+
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import org.sonar.api.rule.Severity;
+import org.sonar.api.rules.RuleType;
+import org.sonar.db.issue.IssueGroupDto;
+import org.sonar.db.rule.SeverityUtil;
+
+class IssueCounter {
+
+  private final Map<RuleType, HighestSeverity> highestSeverityOfUnresolved = new EnumMap<>(RuleType.class);
+  private final Map<RuleType, Effort> effortOfUnresolved = new EnumMap<>(RuleType.class);
+  private final Map<String, Count> unresolvedBySeverity = new HashMap<>();
+  private final Map<RuleType, Count> unresolvedByType = new EnumMap<>(RuleType.class);
+  private final Map<String, Count> byResolution = new HashMap<>();
+  private final Map<String, Count> byStatus = new HashMap<>();
+  private final Count unresolved = new Count();
+
+  IssueCounter(Collection<IssueGroupDto> groups) {
+    for (IssueGroupDto group : groups) {
+      RuleType ruleType = RuleType.valueOf(group.getRuleType());
+      if (group.getResolution() == null) {
+        highestSeverityOfUnresolved
+          .computeIfAbsent(ruleType, k -> new HighestSeverity())
+          .add(group);
+        effortOfUnresolved
+          .computeIfAbsent(ruleType, k -> new Effort())
+          .add(group);
+        unresolvedBySeverity
+          .computeIfAbsent(group.getSeverity(), k -> new Count())
+          .add(group);
+        unresolvedByType
+          .computeIfAbsent(ruleType, k -> new Count())
+          .add(group);
+        unresolved.add(group);
+      } else {
+        byResolution
+          .computeIfAbsent(group.getResolution(), k -> new Count())
+          .add(group);
+      }
+      if (group.getStatus() != null) {
+        byStatus
+          .computeIfAbsent(group.getStatus(), k -> new Count())
+          .add(group);
+      }
+    }
+  }
+
+  public Optional<String> getHighestSeverityOfUnresolved(RuleType ruleType, boolean onlyInLeak) {
+    return Optional.ofNullable(highestSeverityOfUnresolved.get(ruleType))
+      .map(hs -> hs.severity(onlyInLeak));
+  }
+
+  public double sumEffortOfUnresolved(RuleType type, boolean onlyInLeak) {
+    Effort effort = effortOfUnresolved.get(type);
+    if (effort == null) {
+      return 0.0;
+    }
+    return onlyInLeak ? effort.leak : effort.absolute;
+  }
+
+  public long countUnresolvedBySeverity(String severity, boolean onlyInLeak) {
+    return value(unresolvedBySeverity.get(severity), onlyInLeak);
+  }
+
+  public long countByResolution(String resolution, boolean onlyInLeak) {
+    return value(byResolution.get(resolution), onlyInLeak);
+  }
+
+  public long countUnresolvedByType(RuleType type, boolean onlyInLeak) {
+    return value(unresolvedByType.get(type), onlyInLeak);
+  }
+
+  public long countByStatus(String status, boolean onlyInLeak) {
+    return value(byStatus.get(status), onlyInLeak);
+  }
+
+  public long countUnresolved(boolean onlyInLeak) {
+    return value(unresolved, onlyInLeak);
+  }
+
+  private static long value(@Nullable Count count, boolean onlyInLeak) {
+    if (count == null) {
+      return 0;
+    }
+    return onlyInLeak ? count.leak : count.absolute;
+  }
+
+  private static class Count {
+    private long absolute = 0L;
+    private long leak = 0L;
+
+    void add(IssueGroupDto group) {
+      absolute += group.getCount();
+      if (group.isInLeak()) {
+        leak += group.getCount();
+      }
+    }
+  }
+
+  private static class Effort {
+    private double absolute = 0.0;
+    private double leak = 0.0;
+
+    void add(IssueGroupDto group) {
+      absolute += group.getEffort();
+      if (group.isInLeak()) {
+        leak += group.getEffort();
+      }
+    }
+  }
+
+  private static class HighestSeverity {
+    private int absolute = SeverityUtil.getOrdinalFromSeverity(Severity.INFO);
+    private int leak = SeverityUtil.getOrdinalFromSeverity(Severity.INFO);
+
+    void add(IssueGroupDto group) {
+      int severity = SeverityUtil.getOrdinalFromSeverity(group.getSeverity());
+      absolute = Math.max(severity, absolute);
+      if (group.isInLeak()) {
+        leak = Math.max(severity, leak);
+      }
+    }
+
+    String severity(boolean inLeak) {
+      return SeverityUtil.getSeverityFromOrdinal(inLeak ? leak : absolute);
+    }
+  }
+}
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 (file)
index 0000000..521e65b
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.measure.live;
+
+import java.util.Collection;
+import java.util.Optional;
+import java.util.function.BiConsumer;
+import org.sonar.api.measures.Metric;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.server.computation.task.projectanalysis.qualitymodel.DebtRatingGrid;
+import org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating;
+
+import static java.util.Collections.emptyList;
+
+class IssueMetricFormula {
+
+  private final Metric metric;
+  private final boolean onLeak;
+  private final BiConsumer<Context, IssueCounter> formula;
+  private final Collection<Metric> dependentMetrics;
+
+  IssueMetricFormula(Metric metric, boolean onLeak, BiConsumer<Context, IssueCounter> formula) {
+    this(metric, onLeak, formula, emptyList());
+  }
+
+  IssueMetricFormula(Metric metric, boolean onLeak, BiConsumer<Context, IssueCounter> formula, Collection<Metric> dependentMetrics) {
+    this.metric = metric;
+    this.onLeak = onLeak;
+    this.formula = formula;
+    this.dependentMetrics = dependentMetrics;
+  }
+
+  Metric getMetric() {
+    return metric;
+  }
+
+  boolean isOnLeak() {
+    return onLeak;
+  }
+
+  Collection<Metric> getDependentMetrics() {
+    return dependentMetrics;
+  }
+
+  void compute(Context context, IssueCounter issues) {
+    formula.accept(context, issues);
+  }
+
+  interface Context {
+    ComponentDto getComponent();
+
+    DebtRatingGrid getDebtRatingGrid();
+
+    /**
+     * Value that was just refreshed, otherwise value as computed
+     * during last analysis.
+     * The metric must be declared in the formula dependencies
+     * (see {@link IssueMetricFormula#getDependentMetrics()}).
+     */
+    Optional<Double> getValue(Metric metric);
+
+    Optional<Double> getLeakValue(Metric metric);
+
+    void setValue(double value);
+
+    void setValue(Rating value);
+
+    void setLeakValue(double value);
+
+    void setLeakValue(Rating value);
+  }
+}
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 (file)
index 0000000..9fc2578
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.measure.live;
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.server.ServerSide;
+
+@ServerSide
+public interface IssueMetricFormulaFactory {
+  List<IssueMetricFormula> getFormulas();
+
+  Set<Metric> getFormulaMetrics();
+
+  static Set<Metric> extractMetrics(List<IssueMetricFormula> formulas) {
+    return formulas.stream()
+      .flatMap(f -> Stream.concat(Stream.of(f.getMetric()), f.getDependentMetrics().stream()))
+      .collect(Collectors.toSet());
+  }
+}
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 (file)
index 0000000..4044e5d
--- /dev/null
@@ -0,0 +1,200 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.measure.live;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.rule.Severity;
+import org.sonar.api.rules.RuleType;
+import org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating;
+
+import static java.util.Arrays.asList;
+import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.RATING_BY_SEVERITY;
+
+public class IssueMetricFormulaFactoryImpl implements IssueMetricFormulaFactory {
+
+  private static final List<IssueMetricFormula> FORMULAS = asList(
+    new IssueMetricFormula(CoreMetrics.CODE_SMELLS, false,
+      (context, issues) -> context.setValue(issues.countUnresolvedByType(RuleType.CODE_SMELL, false))),
+
+    new IssueMetricFormula(CoreMetrics.BUGS, false,
+      (context, issues) -> context.setValue(issues.countUnresolvedByType(RuleType.BUG, false))),
+
+    new IssueMetricFormula(CoreMetrics.VULNERABILITIES, false,
+      (context, issues) -> context.setValue(issues.countUnresolvedByType(RuleType.VULNERABILITY, false))),
+
+    new IssueMetricFormula(CoreMetrics.VIOLATIONS, false,
+      (context, issues) -> context.setValue(issues.countUnresolved(false))),
+
+    new IssueMetricFormula(CoreMetrics.BLOCKER_VIOLATIONS, false,
+      (context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.BLOCKER, false))),
+
+    new IssueMetricFormula(CoreMetrics.CRITICAL_VIOLATIONS, false,
+      (context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.CRITICAL, false))),
+
+    new IssueMetricFormula(CoreMetrics.MAJOR_VIOLATIONS, false,
+      (context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.MAJOR, false))),
+
+    new IssueMetricFormula(CoreMetrics.MINOR_VIOLATIONS, false,
+      (context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.MINOR, false))),
+
+    new IssueMetricFormula(CoreMetrics.INFO_VIOLATIONS, false,
+      (context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.INFO, false))),
+
+    new IssueMetricFormula(CoreMetrics.FALSE_POSITIVE_ISSUES, false,
+      (context, issues) -> context.setValue(issues.countByResolution(Issue.RESOLUTION_FALSE_POSITIVE, false))),
+
+    new IssueMetricFormula(CoreMetrics.WONT_FIX_ISSUES, false,
+      (context, issues) -> context.setValue(issues.countByResolution(Issue.RESOLUTION_WONT_FIX, false))),
+
+    new IssueMetricFormula(CoreMetrics.OPEN_ISSUES, false,
+      (context, issues) -> context.setValue(issues.countByStatus(Issue.STATUS_OPEN, false))),
+
+    new IssueMetricFormula(CoreMetrics.REOPENED_ISSUES, false,
+      (context, issues) -> context.setValue(issues.countByStatus(Issue.STATUS_REOPENED, false))),
+
+    new IssueMetricFormula(CoreMetrics.CONFIRMED_ISSUES, false,
+      (context, issues) -> context.setValue(issues.countByStatus(Issue.STATUS_CONFIRMED, false))),
+
+    new IssueMetricFormula(CoreMetrics.TECHNICAL_DEBT, false,
+      (context, issues) -> context.setValue(issues.sumEffortOfUnresolved(RuleType.CODE_SMELL, false))),
+
+    new IssueMetricFormula(CoreMetrics.RELIABILITY_REMEDIATION_EFFORT, false,
+      (context, issues) -> context.setValue(issues.sumEffortOfUnresolved(RuleType.BUG, false))),
+
+    new IssueMetricFormula(CoreMetrics.SECURITY_REMEDIATION_EFFORT, false,
+      (context, issues) -> context.setValue(issues.sumEffortOfUnresolved(RuleType.VULNERABILITY, false))),
+
+    new IssueMetricFormula(CoreMetrics.SQALE_DEBT_RATIO, false,
+      (context, issues) -> context.setValue(100.0 * debtDensity(context)),
+      asList(CoreMetrics.TECHNICAL_DEBT, CoreMetrics.DEVELOPMENT_COST)),
+
+    new IssueMetricFormula(CoreMetrics.SQALE_RATING, false,
+      (context, issues) -> context
+        .setValue(context.getDebtRatingGrid().getRatingForDensity(debtDensity(context))),
+      asList(CoreMetrics.TECHNICAL_DEBT, CoreMetrics.DEVELOPMENT_COST)),
+
+    new IssueMetricFormula(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, false,
+      (context, issues) -> context.setValue(effortToReachMaintainabilityRatingA(context)), asList(CoreMetrics.TECHNICAL_DEBT, CoreMetrics.DEVELOPMENT_COST)),
+
+    new IssueMetricFormula(CoreMetrics.RELIABILITY_RATING, false,
+      (context, issues) -> context.setValue(RATING_BY_SEVERITY.get(issues.getHighestSeverityOfUnresolved(RuleType.BUG, false).orElse(Severity.INFO)))),
+
+    new IssueMetricFormula(CoreMetrics.SECURITY_RATING, false,
+      (context, issues) -> context.setValue(RATING_BY_SEVERITY.get(issues.getHighestSeverityOfUnresolved(RuleType.VULNERABILITY, false).orElse(Severity.INFO)))),
+
+    new IssueMetricFormula(CoreMetrics.NEW_CODE_SMELLS, true,
+      (context, issues) -> context.setLeakValue(issues.countUnresolvedByType(RuleType.CODE_SMELL, true))),
+
+    new IssueMetricFormula(CoreMetrics.NEW_BUGS, true,
+      (context, issues) -> context.setLeakValue(issues.countUnresolvedByType(RuleType.BUG, true))),
+
+    new IssueMetricFormula(CoreMetrics.NEW_VULNERABILITIES, true,
+      (context, issues) -> context.setLeakValue(issues.countUnresolvedByType(RuleType.VULNERABILITY, true))),
+
+    new IssueMetricFormula(CoreMetrics.NEW_VIOLATIONS, true,
+      (context, issues) -> context.setLeakValue(issues.countUnresolved(true))),
+
+    new IssueMetricFormula(CoreMetrics.NEW_BLOCKER_VIOLATIONS, true,
+      (context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.BLOCKER, true))),
+
+    new IssueMetricFormula(CoreMetrics.NEW_CRITICAL_VIOLATIONS, true,
+      (context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.CRITICAL, true))),
+
+    new IssueMetricFormula(CoreMetrics.NEW_MAJOR_VIOLATIONS, true,
+      (context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.MAJOR, true))),
+
+    new IssueMetricFormula(CoreMetrics.NEW_MINOR_VIOLATIONS, true,
+      (context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.MINOR, true))),
+
+    new IssueMetricFormula(CoreMetrics.NEW_INFO_VIOLATIONS, true,
+      (context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.INFO, true))),
+
+    new IssueMetricFormula(CoreMetrics.NEW_TECHNICAL_DEBT, true,
+      (context, issues) -> context.setLeakValue(issues.sumEffortOfUnresolved(RuleType.CODE_SMELL, true))),
+
+    new IssueMetricFormula(CoreMetrics.NEW_RELIABILITY_REMEDIATION_EFFORT, true,
+      (context, issues) -> context.setLeakValue(issues.sumEffortOfUnresolved(RuleType.BUG, true))),
+
+    new IssueMetricFormula(CoreMetrics.NEW_SECURITY_REMEDIATION_EFFORT, true,
+      (context, issues) -> context.setLeakValue(issues.sumEffortOfUnresolved(RuleType.VULNERABILITY, true))),
+
+    new IssueMetricFormula(CoreMetrics.NEW_RELIABILITY_RATING, true,
+      (context, issues) -> {
+        String highestSeverity = issues.getHighestSeverityOfUnresolved(RuleType.BUG, true).orElse(Severity.INFO);
+        context.setLeakValue(RATING_BY_SEVERITY.get(highestSeverity));
+      }),
+
+    new IssueMetricFormula(CoreMetrics.NEW_SECURITY_RATING, true,
+      (context, issues) -> {
+        String highestSeverity = issues.getHighestSeverityOfUnresolved(RuleType.VULNERABILITY, true).orElse(Severity.INFO);
+        context.setLeakValue(RATING_BY_SEVERITY.get(highestSeverity));
+      }),
+
+    new IssueMetricFormula(CoreMetrics.NEW_SQALE_DEBT_RATIO, true,
+      (context, issues) -> context.setLeakValue(100.0 * newDebtDensity(context)),
+      asList(CoreMetrics.NEW_TECHNICAL_DEBT, CoreMetrics.NEW_DEVELOPMENT_COST)),
+
+    new IssueMetricFormula(CoreMetrics.NEW_MAINTAINABILITY_RATING, true,
+      (context, issues) -> context.setLeakValue(context.getDebtRatingGrid().getRatingForDensity(
+        newDebtDensity(context))),
+      asList(CoreMetrics.NEW_TECHNICAL_DEBT, CoreMetrics.NEW_DEVELOPMENT_COST)));
+
+  private static final Set<Metric> FORMULA_METRICS = IssueMetricFormulaFactory.extractMetrics(FORMULAS);
+
+  private static double debtDensity(IssueMetricFormula.Context context) {
+    double debt = Math.max(context.getValue(CoreMetrics.TECHNICAL_DEBT).orElse(0.0), 0.0);
+    Optional<Double> devCost = context.getValue(CoreMetrics.DEVELOPMENT_COST);
+    if (devCost.isPresent() && Double.doubleToRawLongBits(devCost.get()) > 0L) {
+      return debt / devCost.get();
+    }
+    return 0d;
+  }
+
+  private static double newDebtDensity(IssueMetricFormula.Context context) {
+    double debt = Math.max(context.getLeakValue(CoreMetrics.NEW_TECHNICAL_DEBT).orElse(0.0), 0.0);
+    Optional<Double> devCost = context.getLeakValue(CoreMetrics.NEW_DEVELOPMENT_COST);
+    if (devCost.isPresent() && Double.doubleToRawLongBits(devCost.get()) > 0L) {
+      return debt / devCost.get();
+    }
+    return 0d;
+  }
+
+  private static double effortToReachMaintainabilityRatingA(IssueMetricFormula.Context context) {
+    double developmentCost = context.getValue(CoreMetrics.DEVELOPMENT_COST).orElse(0.0);
+    double effort = context.getValue(CoreMetrics.TECHNICAL_DEBT).orElse(0.0);
+    double upperGradeCost = context.getDebtRatingGrid().getGradeLowerBound(Rating.B) * developmentCost;
+    return upperGradeCost < effort ? (effort - upperGradeCost) : 0.0;
+  }
+
+  @Override
+  public List<IssueMetricFormula> getFormulas() {
+    return FORMULAS;
+  }
+
+  @Override
+  public Set<Metric> getFormulaMetrics() {
+    return FORMULA_METRICS;
+  }
+}
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 (file)
index 0000000..74b51a0
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.measure.live;
+
+import java.util.Collection;
+import java.util.List;
+import org.sonar.api.server.ServerSide;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.server.qualitygate.changeevent.QGChangeEvent;
+
+/**
+ * Refresh and persist the measures of some files, directories, modules
+ * or projects. Measures include status of quality gate.
+ *
+ * Touching a file updates the related directory, module and project.
+ * Status of Quality gate is refreshed but webhooks are not triggered.
+ *
+ * Branches are supported.
+ */
+@ServerSide
+public interface LiveMeasureComputer {
+
+  List<QGChangeEvent> refresh(DbSession dbSession, Collection<ComponentDto> components);
+
+}
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 (file)
index 0000000..3f6f986
--- /dev/null
@@ -0,0 +1,242 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.measure.live;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.measures.Metric;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.SnapshotDto;
+import org.sonar.db.measure.LiveMeasureDto;
+import org.sonar.db.metric.MetricDto;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.server.computation.task.projectanalysis.qualitymodel.DebtRatingGrid;
+import org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating;
+import org.sonar.server.es.ProjectIndexer;
+import org.sonar.server.es.ProjectIndexers;
+import org.sonar.server.qualitygate.EvaluatedQualityGate;
+import org.sonar.server.qualitygate.QualityGate;
+import org.sonar.server.qualitygate.changeevent.QGChangeEvent;
+import org.sonar.server.settings.ProjectConfigurationLoader;
+
+import static com.google.common.base.Preconditions.checkState;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singleton;
+import static java.util.stream.Collectors.groupingBy;
+import static org.sonar.core.util.stream.MoreCollectors.toArrayList;
+import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
+
+public class LiveMeasureComputerImpl implements LiveMeasureComputer {
+
+  private final DbClient dbClient;
+  private final IssueMetricFormulaFactory formulaFactory;
+  private final LiveQualityGateComputer qGateComputer;
+  private final ProjectConfigurationLoader projectConfigurationLoader;
+  private final ProjectIndexers projectIndexer;
+
+  public LiveMeasureComputerImpl(DbClient dbClient, IssueMetricFormulaFactory formulaFactory,
+    LiveQualityGateComputer qGateComputer, ProjectConfigurationLoader projectConfigurationLoader, ProjectIndexers projectIndexer) {
+    this.dbClient = dbClient;
+    this.formulaFactory = formulaFactory;
+    this.qGateComputer = qGateComputer;
+    this.projectConfigurationLoader = projectConfigurationLoader;
+    this.projectIndexer = projectIndexer;
+  }
+
+  @Override
+  public List<QGChangeEvent> refresh(DbSession dbSession, Collection<ComponentDto> components) {
+    if (components.isEmpty()) {
+      return emptyList();
+    }
+
+    List<QGChangeEvent> result = new ArrayList<>();
+    Map<String, List<ComponentDto>> componentsByProjectUuid = components.stream().collect(groupingBy(ComponentDto::projectUuid));
+    for (List<ComponentDto> groupedComponents : componentsByProjectUuid.values()) {
+      Optional<QGChangeEvent> qgChangeEvent = refreshComponentsOnSameProject(dbSession, groupedComponents);
+      qgChangeEvent.ifPresent(result::add);
+    }
+    return result;
+  }
+
+  private Optional<QGChangeEvent> refreshComponentsOnSameProject(DbSession dbSession, List<ComponentDto> touchedComponents) {
+    // load all the components to be refreshed, including their ancestors
+    List<ComponentDto> components = loadTreeOfComponents(dbSession, touchedComponents);
+    ComponentDto project = findProject(components);
+    OrganizationDto organization = loadOrganization(dbSession, project);
+    BranchDto branch = loadBranch(dbSession, project);
+
+    Optional<SnapshotDto> lastAnalysis = dbClient.snapshotDao().selectLastAnalysisByRootComponentUuid(dbSession, project.uuid());
+    if (!lastAnalysis.isPresent()) {
+      return Optional.empty();
+    }
+    Optional<Long> beginningOfLeakPeriod = lastAnalysis.map(SnapshotDto::getPeriodDate);
+
+    QualityGate qualityGate = qGateComputer.loadQualityGate(dbSession, organization, project, branch);
+    Collection<String> metricKeys = getKeysOfAllInvolvedMetrics(qualityGate);
+
+    Map<Integer, MetricDto> metricsPerId = dbClient.metricDao().selectByKeys(dbSession, metricKeys).stream()
+      .collect(uniqueIndex(MetricDto::getId));
+    List<String> componentUuids = components.stream().map(ComponentDto::uuid).collect(toArrayList(components.size()));
+    List<LiveMeasureDto> dbMeasures = dbClient.liveMeasureDao().selectByComponentUuidsAndMetricIds(dbSession, componentUuids, metricsPerId.keySet());
+
+    Configuration config = projectConfigurationLoader.loadProjectConfiguration(dbSession, project);
+    DebtRatingGrid debtRatingGrid = new DebtRatingGrid(config);
+
+    MeasureMatrix matrix = new MeasureMatrix(components, metricsPerId.values(), dbMeasures);
+    components.forEach(c -> {
+      IssueCounter issueCounter = new IssueCounter(dbClient.issueDao().selectIssueGroupsByBaseComponent(dbSession, c, beginningOfLeakPeriod.orElse(Long.MAX_VALUE)));
+      FormulaContextImpl context = new FormulaContextImpl(matrix, debtRatingGrid);
+      for (IssueMetricFormula formula : formulaFactory.getFormulas()) {
+        // exclude leak formulas when leak period is not defined
+        if (beginningOfLeakPeriod.isPresent() || !formula.isOnLeak()) {
+          context.change(c, formula);
+          try {
+            formula.compute(context, issueCounter);
+          } catch (RuntimeException e) {
+            throw new IllegalStateException("Fail to compute " + formula.getMetric().getKey() + " on " + context.getComponent().getDbKey(), e);
+          }
+        }
+      }
+    });
+
+    EvaluatedQualityGate evaluatedQualityGate = qGateComputer.refreshGateStatus(project, qualityGate, matrix);
+
+    // persist the measures that have been created or updated
+    matrix.getChanged().forEach(m -> dbClient.liveMeasureDao().insertOrUpdate(dbSession, m, null));
+    projectIndexer.commitAndIndex(dbSession, singleton(project), ProjectIndexer.Cause.MEASURE_CHANGE);
+
+    return Optional.of(new QGChangeEvent(project, branch, lastAnalysis.get(), config, () -> Optional.of(evaluatedQualityGate)));
+  }
+
+  private List<ComponentDto> loadTreeOfComponents(DbSession dbSession, List<ComponentDto> touchedComponents) {
+    Set<String> componentUuids = new HashSet<>();
+    for (ComponentDto component : touchedComponents) {
+      componentUuids.add(component.uuid());
+      // ancestors, excluding self
+      componentUuids.addAll(component.getUuidPathAsList());
+    }
+    // Contrary to the formulas in Compute Engine,
+    // measures do not aggregate values of descendant components.
+    // As a consequence nodes do not need to be sorted. Formulas can be applied
+    // on components in any order.
+    return dbClient.componentDao().selectByUuids(dbSession, componentUuids);
+  }
+
+  private Set<String> getKeysOfAllInvolvedMetrics(QualityGate gate) {
+    Set<String> metricKeys = new HashSet<>();
+    for (Metric metric : formulaFactory.getFormulaMetrics()) {
+      metricKeys.add(metric.getKey());
+    }
+    metricKeys.addAll(qGateComputer.getMetricsRelatedTo(gate));
+    return metricKeys;
+  }
+
+  private static ComponentDto findProject(Collection<ComponentDto> components) {
+    return components.stream().filter(ComponentDto::isRootProject).findFirst()
+      .orElseThrow(() -> new IllegalStateException("No project found in " + components));
+  }
+
+  private BranchDto loadBranch(DbSession dbSession, ComponentDto project) {
+    return dbClient.branchDao().selectByUuid(dbSession, project.uuid())
+      .orElseThrow(() -> new IllegalStateException("Branch not found: " + project.uuid()));
+  }
+
+  private OrganizationDto loadOrganization(DbSession dbSession, ComponentDto project) {
+    String organizationUuid = project.getOrganizationUuid();
+    return dbClient.organizationDao().selectByUuid(dbSession, organizationUuid)
+      .orElseThrow(() -> new IllegalStateException("No organization with UUID " + organizationUuid));
+  }
+
+  private static class FormulaContextImpl implements IssueMetricFormula.Context {
+    private final MeasureMatrix matrix;
+    private final DebtRatingGrid debtRatingGrid;
+    private ComponentDto currentComponent;
+    private IssueMetricFormula currentFormula;
+
+    private FormulaContextImpl(MeasureMatrix matrix, DebtRatingGrid debtRatingGrid) {
+      this.matrix = matrix;
+      this.debtRatingGrid = debtRatingGrid;
+    }
+
+    private void change(ComponentDto component, IssueMetricFormula formula) {
+      this.currentComponent = component;
+      this.currentFormula = formula;
+    }
+
+    @Override
+    public ComponentDto getComponent() {
+      return currentComponent;
+    }
+
+    @Override
+    public DebtRatingGrid getDebtRatingGrid() {
+      return debtRatingGrid;
+    }
+
+    @Override
+    public Optional<Double> getValue(Metric metric) {
+      Optional<LiveMeasureDto> measure = matrix.getMeasure(currentComponent, metric.getKey());
+      return measure.map(LiveMeasureDto::getValue);
+    }
+
+    @Override
+    public Optional<Double> getLeakValue(Metric metric) {
+      Optional<LiveMeasureDto> measure = matrix.getMeasure(currentComponent, metric.getKey());
+      return measure.map(LiveMeasureDto::getVariation);
+    }
+
+    @Override
+    public void setValue(double value) {
+      String metricKey = currentFormula.getMetric().getKey();
+      checkState(!currentFormula.isOnLeak(), "Formula of metric %s accepts only leak values", metricKey);
+      matrix.setValue(currentComponent, metricKey, value);
+    }
+
+    @Override
+    public void setLeakValue(double value) {
+      String metricKey = currentFormula.getMetric().getKey();
+      checkState(currentFormula.isOnLeak(), "Formula of metric %s does not accept leak values", metricKey);
+      matrix.setLeakValue(currentComponent, metricKey, value);
+    }
+
+    @Override
+    public void setValue(Rating value) {
+      String metricKey = currentFormula.getMetric().getKey();
+      checkState(!currentFormula.isOnLeak(), "Formula of metric %s accepts only leak values", metricKey);
+      matrix.setValue(currentComponent, metricKey, value);
+    }
+
+    @Override
+    public void setLeakValue(Rating value) {
+      String metricKey = currentFormula.getMetric().getKey();
+      checkState(currentFormula.isOnLeak(), "Formula of metric %s does not accept leak values", metricKey);
+      matrix.setLeakValue(currentComponent, metricKey, value);
+    }
+  }
+}
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 (file)
index 0000000..116bd90
--- /dev/null
@@ -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 (file)
index 0000000..e59d5d3
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.measure.live;
+
+import java.util.Set;
+import org.sonar.api.server.ServerSide;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.server.qualitygate.EvaluatedQualityGate;
+import org.sonar.server.qualitygate.QualityGate;
+
+@ServerSide
+public interface LiveQualityGateComputer {
+
+  QualityGate loadQualityGate(DbSession dbSession, OrganizationDto organization, ComponentDto project, BranchDto branch);
+
+  EvaluatedQualityGate refreshGateStatus(ComponentDto project, QualityGate gate, MeasureMatrix measureMatrix);
+
+  Set<String> getMetricsRelatedTo(QualityGate gate);
+
+}
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 (file)
index 0000000..b7cf692
--- /dev/null
@@ -0,0 +1,151 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.measure.live;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.OptionalDouble;
+import java.util.Set;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Metric;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.BranchType;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.measure.LiveMeasureDto;
+import org.sonar.db.metric.MetricDto;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.db.qualitygate.QualityGateConditionDto;
+import org.sonar.db.qualitygate.QualityGateDto;
+import org.sonar.server.qualitygate.Condition;
+import org.sonar.server.qualitygate.EvaluatedQualityGate;
+import org.sonar.server.qualitygate.QualityGate;
+import org.sonar.server.qualitygate.QualityGateConverter;
+import org.sonar.server.qualitygate.QualityGateEvaluator;
+import org.sonar.server.qualitygate.QualityGateFinder;
+import org.sonar.server.qualitygate.ShortLivingBranchQualityGate;
+
+import static org.sonar.core.util.stream.MoreCollectors.toHashSet;
+import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
+
+public class LiveQualityGateComputerImpl implements LiveQualityGateComputer {
+
+  private final DbClient dbClient;
+  private final QualityGateFinder qGateFinder;
+  private final QualityGateEvaluator evaluator;
+
+  public LiveQualityGateComputerImpl(DbClient dbClient, QualityGateFinder qGateFinder, QualityGateEvaluator evaluator) {
+    this.dbClient = dbClient;
+    this.qGateFinder = qGateFinder;
+    this.evaluator = evaluator;
+  }
+
+  @Override
+  public QualityGate loadQualityGate(DbSession dbSession, OrganizationDto organization, ComponentDto project, BranchDto branch) {
+    if (branch.getBranchType() == BranchType.SHORT) {
+      return ShortLivingBranchQualityGate.GATE;
+    }
+
+    ComponentDto mainProject = project.getMainBranchProjectUuid() == null ? project : dbClient.componentDao().selectOrFailByKey(dbSession, project.getKey());
+    QualityGateDto gateDto = qGateFinder.getQualityGate(dbSession, organization, mainProject).getQualityGate();
+    Collection<QualityGateConditionDto> conditionDtos = dbClient.gateConditionDao().selectForQualityGate(dbSession, gateDto.getId());
+    Set<Integer> metricIds = conditionDtos.stream().map(c -> (int) c.getMetricId())
+      .collect(toHashSet(conditionDtos.size()));
+    Map<Integer, MetricDto> metricsById = dbClient.metricDao().selectByIds(dbSession, metricIds).stream()
+      .collect(uniqueIndex(MetricDto::getId));
+
+    Set<Condition> conditions = conditionDtos.stream().map(conditionDto -> {
+      String metricKey = metricsById.get((int) conditionDto.getMetricId()).getKey();
+      Condition.Operator operator = Condition.Operator.fromDbValue(conditionDto.getOperator());
+      boolean onLeak = Objects.equals(conditionDto.getPeriod(), 1);
+      return new Condition(metricKey, operator, conditionDto.getErrorThreshold(), conditionDto.getWarningThreshold(), onLeak);
+    }).collect(toHashSet(conditionDtos.size()));
+
+    return new QualityGate(String.valueOf(gateDto.getId()), gateDto.getName(), conditions);
+  }
+
+  @Override
+  public EvaluatedQualityGate refreshGateStatus(ComponentDto project, QualityGate gate, MeasureMatrix measureMatrix) {
+    QualityGateEvaluator.Measures measures = metricKey -> {
+      Optional<LiveMeasureDto> liveMeasureDto = measureMatrix.getMeasure(project, metricKey);
+      if (!liveMeasureDto.isPresent()) {
+        return Optional.empty();
+      }
+      MetricDto metric = measureMatrix.getMetric(liveMeasureDto.get().getMetricId());
+      return Optional.of(new LiveMeasure(liveMeasureDto.get(), metric));
+    };
+
+    EvaluatedQualityGate evaluatedGate = evaluator.evaluate(gate, measures);
+
+    measureMatrix.setValue(project, CoreMetrics.ALERT_STATUS_KEY, evaluatedGate.getStatus().name());
+    measureMatrix.setValue(project, CoreMetrics.QUALITY_GATE_DETAILS_KEY, QualityGateConverter.toJson(evaluatedGate));
+
+    return evaluatedGate;
+  }
+
+  @Override
+  public Set<String> getMetricsRelatedTo(QualityGate gate) {
+    Set<String> metricKeys = new HashSet<>();
+    metricKeys.add(CoreMetrics.ALERT_STATUS_KEY);
+    metricKeys.add(CoreMetrics.QUALITY_GATE_DETAILS_KEY);
+    metricKeys.addAll(evaluator.getMetricKeys(gate));
+    return metricKeys;
+  }
+
+  private static class LiveMeasure implements QualityGateEvaluator.Measure {
+    private final LiveMeasureDto dto;
+    private final MetricDto metric;
+
+    LiveMeasure(LiveMeasureDto dto, MetricDto metric) {
+      this.dto = dto;
+      this.metric = metric;
+    }
+
+    @Override
+    public Metric.ValueType getType() {
+      return Metric.ValueType.valueOf(metric.getValueType());
+    }
+
+    @Override
+    public OptionalDouble getValue() {
+      if (dto.getValue() == null) {
+        return OptionalDouble.empty();
+      }
+      return OptionalDouble.of(dto.getValue());
+    }
+
+    @Override
+    public Optional<String> getStringValue() {
+      return Optional.ofNullable(dto.getTextValue());
+    }
+
+    @Override
+    public OptionalDouble getLeakValue() {
+      if (dto.getVariation() == null) {
+        return OptionalDouble.empty();
+      }
+      return OptionalDouble.of(dto.getVariation());
+    }
+  }
+}
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 (file)
index 0000000..0dad8f5
--- /dev/null
@@ -0,0 +1,203 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.measure.live;
+
+import com.google.common.collect.ArrayTable;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Table;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import javax.annotation.Nullable;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.measure.LiveMeasureDto;
+import org.sonar.db.metric.MetricDto;
+import org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Keep the measures in memory during refresh of live measures:
+ * <ul>
+ *   <li>the values of last analysis, restricted to the needed metrics</li>
+ *   <li>the refreshed values</li>
+ * </ul>
+ */
+class MeasureMatrix {
+
+  // component uuid -> metric key -> measure
+  private final Table<String, String, MeasureCell> table;
+
+  private final Map<String, MetricDto> metricsByKeys = new HashMap<>();
+  private final Map<Integer, MetricDto> metricsByIds = new HashMap<>();
+
+  MeasureMatrix(Collection<ComponentDto> components, Collection<MetricDto> metrics, List<LiveMeasureDto> dbMeasures) {
+    for (MetricDto metric : metrics) {
+      this.metricsByKeys.put(metric.getKey(), metric);
+      this.metricsByIds.put(metric.getId(), metric);
+    }
+    this.table = ArrayTable.create(Collections2.transform(components, ComponentDto::uuid), metricsByKeys.keySet());
+    for (LiveMeasureDto dbMeasure : dbMeasures) {
+      table.put(dbMeasure.getComponentUuid(), metricsByIds.get(dbMeasure.getMetricId()).getKey(), new MeasureCell(dbMeasure, false));
+    }
+  }
+
+  MetricDto getMetric(int id) {
+    return requireNonNull(metricsByIds.get(id), () -> String.format("Metric with id %d not found", id));
+  }
+
+  private MetricDto getMetric(String key) {
+    return requireNonNull(metricsByKeys.get(key), () -> String.format("Metric with key %s not found", key));
+  }
+
+  Optional<LiveMeasureDto> getMeasure(ComponentDto component, String metricKey) {
+    checkArgument(table.containsColumn(metricKey), "Metric with key %s is not registered", metricKey);
+    MeasureCell cell = table.get(component.uuid(), metricKey);
+    return cell == null ? Optional.empty() : Optional.of(cell.measure);
+  }
+
+  void setValue(ComponentDto component, String metricKey, double value) {
+    changeCell(component, metricKey, m -> {
+      MetricDto metric = getMetric(metricKey);
+      double newValue = scale(metric, value);
+
+      Double initialValue = m.getValue();
+      if (initialValue != null && Double.compare(initialValue, newValue) == 0) {
+        return false;
+      }
+      m.setValue(newValue);
+      Double initialVariation = m.getVariation();
+      if (initialValue != null && initialVariation != null) {
+        double leakInitialValue = initialValue - initialVariation;
+        m.setVariation(scale(metric, value - leakInitialValue));
+      }
+      return true;
+    });
+  }
+
+  void setValue(ComponentDto component, String metricKey, Rating value) {
+    changeCell(component, metricKey, m -> {
+      Double initialValue = m.getValue();
+      if (initialValue != null && Double.compare(initialValue, (double) value.getIndex()) == 0) {
+        return false;
+      }
+      m.setData(value.name());
+      m.setValue((double) value.getIndex());
+
+      Double initialVariation = m.getVariation();
+      if (initialValue != null && initialVariation != null) {
+        double leakInitialValue = initialValue - initialVariation;
+        m.setVariation(value.getIndex() - leakInitialValue);
+      }
+      return true;
+    });
+  }
+
+  void setValue(ComponentDto component, String metricKey, @Nullable String data) {
+    changeCell(component, metricKey, m -> {
+      if (Objects.equals(m.getDataAsString(), data)) {
+        return false;
+      }
+      m.setData(data);
+      return true;
+    });
+  }
+
+  void setLeakValue(ComponentDto component, String metricKey, double variation) {
+    changeCell(component, metricKey, c -> {
+      double newVariation = scale(getMetric(metricKey), variation);
+      if (c.getVariation() != null && Double.compare(c.getVariation(), newVariation) == 0) {
+        return false;
+      }
+      MetricDto metric = metricsByKeys.get(metricKey);
+      c.setVariation(scale(metric, variation));
+      return true;
+    });
+  }
+
+  void setLeakValue(ComponentDto component, String metricKey, Rating variation) {
+    setLeakValue(component, metricKey, (double) variation.getIndex());
+  }
+
+  Stream<LiveMeasureDto> getChanged() {
+    return table.values()
+      .stream()
+      .filter(Objects::nonNull)
+      .filter(MeasureCell::isChanged)
+      .map(MeasureCell::getMeasure);
+  }
+
+  private void changeCell(ComponentDto component, String metricKey, Function<LiveMeasureDto, Boolean> changer) {
+    MeasureCell cell = table.get(component.uuid(), metricKey);
+    if (cell == null) {
+      LiveMeasureDto measure = new LiveMeasureDto()
+        .setComponentUuid(component.uuid())
+        .setProjectUuid(component.projectUuid())
+        .setMetricId(metricsByKeys.get(metricKey).getId());
+      cell = new MeasureCell(measure, true);
+      table.put(component.uuid(), metricKey, cell);
+      changer.apply(cell.getMeasure());
+    } else if (changer.apply(cell.getMeasure())) {
+      cell.setChanged(true);
+    }
+  }
+
+  /**
+   * Round a measure value by applying the scale defined on the metric.
+   * Example: scale(0.1234) returns 0.12 if metric scale is 2
+   */
+  private static double scale(MetricDto metric, double value) {
+    if (metric.getDecimalScale() == null) {
+      return value;
+    }
+    BigDecimal bd = BigDecimal.valueOf(value);
+    return bd.setScale(metric.getDecimalScale(), RoundingMode.HALF_UP).doubleValue();
+  }
+
+  private static class MeasureCell {
+    private final LiveMeasureDto measure;
+    private boolean changed;
+
+    private MeasureCell(LiveMeasureDto measure, boolean changed) {
+      this.measure = measure;
+      this.changed = changed;
+    }
+
+    public LiveMeasureDto getMeasure() {
+      return measure;
+    }
+
+    public boolean isChanged() {
+      return changed;
+    }
+
+    public void setChanged(boolean b) {
+      this.changed = b;
+    }
+  }
+}
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 (file)
index 0000000..4e4f175
--- /dev/null
@@ -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;
index a4a59852c7f8f6b0f2fcd30a2998740676a3c77e..8ede31ea203d322ec71c447defc906e76a127b2a 100644 (file)
@@ -216,7 +216,7 @@ public class ComponentAction implements MeasuresWsAction {
 
   private List<LiveMeasureDto> searchMeasures(DbSession dbSession, ComponentDto component, List<MetricDto> metrics) {
     List<Integer> metricIds = Lists.transform(metrics, MetricDto::getId);
-    List<LiveMeasureDto> measures = dbClient.liveMeasureDao().selectByComponentUuids(dbSession, singletonList(component.uuid()), metricIds);
+    List<LiveMeasureDto> measures = dbClient.liveMeasureDao().selectByComponentUuidsAndMetricIds(dbSession, singletonList(component.uuid()), metricIds);
     addBestValuesToMeasures(measures, component, metrics);
     return measures;
   }
index 30424bc006b2938cc226c61cd2a5f6e5279658c1..b58abdfb97f029405060ca2e48eba367818ffa00 100644 (file)
@@ -162,7 +162,7 @@ public class SearchAction implements MeasuresWsAction {
     }
 
     private List<LiveMeasureDto> searchMeasures() {
-      return dbClient.liveMeasureDao().selectByComponentUuids(dbSession,
+      return dbClient.liveMeasureDao().selectByComponentUuidsAndMetricIds(dbSession,
         projects.stream().map(ComponentDto::uuid).collect(MoreCollectors.toArrayList(projects.size())),
         metrics.stream().map(MetricDto::getId).collect(MoreCollectors.toArrayList(metrics.size())));
     }
index c3b7f5560009635a254eaec2bc45cc1d5cab6aab..1a9ab35efec0808bd29ebf09b4872ccb69672869 100644 (file)
@@ -45,7 +45,6 @@ import org.sonar.server.permission.index.PermissionIndexerDao.Dto;
 import static java.util.Collections.emptyList;
 import static org.sonar.core.util.stream.MoreCollectors.toArrayList;
 import static org.sonar.core.util.stream.MoreCollectors.toSet;
-import static org.sonar.server.es.DefaultIndexSettings.REFRESH_IMMEDIATE;
 
 /**
  * Populates the types "authorization" of each index requiring project
@@ -100,9 +99,10 @@ public class PermissionIndexer implements ProjectIndexer {
   @Override
   public Collection<EsQueueDto> prepareForRecovery(DbSession dbSession, Collection<String> projectUuids, ProjectIndexer.Cause cause) {
     switch (cause) {
+      case MEASURE_CHANGE:
       case PROJECT_KEY_UPDATE:
       case PROJECT_TAGS_UPDATE:
-        // nothing to change, project key and tags are not part of this index
+        // nothing to change. Measures, project key and tags are not part of this index
         return emptyList();
 
       case PROJECT_CREATION:
index dfbee585adf82f867d3834d17c1ef89997c70099..db08d7499b5a9db43ebf4fb0430ea84bd38ac8b7 100644 (file)
@@ -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,
index 26a2691d5ef0fba1c8426e321ed6fe933f08eb98..3319b92dd4ac75ccf4928f62d7764454ca17ad93 100644 (file)
@@ -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 (file)
index 0000000..f154d34
--- /dev/null
@@ -0,0 +1,181 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.qualitygate;
+
+import java.util.EnumSet;
+import java.util.Optional;
+import java.util.Set;
+import javax.annotation.CheckForNull;
+import org.sonar.api.measures.Metric.ValueType;
+import org.sonar.server.qualitygate.EvaluatedCondition.EvaluationStatus;
+
+import static java.util.Optional.of;
+import static org.sonar.api.measures.Metric.ValueType.BOOL;
+import static org.sonar.api.measures.Metric.ValueType.FLOAT;
+import static org.sonar.api.measures.Metric.ValueType.INT;
+import static org.sonar.api.measures.Metric.ValueType.MILLISEC;
+import static org.sonar.api.measures.Metric.ValueType.PERCENT;
+import static org.sonar.api.measures.Metric.ValueType.RATING;
+import static org.sonar.api.measures.Metric.ValueType.WORK_DUR;
+
+class ConditionEvaluator {
+
+  private static final Set<ValueType> NUMERICAL_TYPES = EnumSet.of(BOOL, INT, RATING, FLOAT, MILLISEC, PERCENT, WORK_DUR);
+
+  private ConditionEvaluator() {
+    // prevent instantiation
+  }
+
+  /**
+   * Evaluates the condition for the specified measure
+   */
+  static EvaluatedCondition evaluate(Condition condition, QualityGateEvaluator.Measures measures) {
+    Optional<QualityGateEvaluator.Measure> measure = measures.get(condition.getMetricKey());
+    if (!measure.isPresent()) {
+      return new EvaluatedCondition(condition, EvaluationStatus.OK, null);
+    }
+
+    Optional<Comparable> value = getMeasureValue(condition, measure.get());
+    if (!value.isPresent()) {
+      return new EvaluatedCondition(condition, EvaluationStatus.OK, null);
+    }
+
+    ValueType type = measure.get().getType();
+    return evaluateCondition(condition, type, value.get(), true)
+      .orElseGet(() -> evaluateCondition(condition, type, value.get(), false)
+        .orElseGet(() -> new EvaluatedCondition(condition, EvaluationStatus.OK, value.get().toString())));
+  }
+
+  /**
+   * Evaluates the error or warning condition. Returns empty if threshold or measure value is not defined.
+   */
+  private static Optional<EvaluatedCondition> evaluateCondition(Condition condition, ValueType type, Comparable value, boolean error) {
+    Optional<Comparable> threshold = getThreshold(condition, type, error);
+    if (!threshold.isPresent()) {
+      return Optional.empty();
+    }
+
+    if (reachThreshold(value, threshold.get(), condition)) {
+      EvaluationStatus status = error ? EvaluationStatus.ERROR : EvaluationStatus.WARN;
+      return of(new EvaluatedCondition(condition, status, value.toString()));
+    }
+    return Optional.empty();
+  }
+
+  private static Optional<Comparable> getThreshold(Condition condition, ValueType valueType, boolean error) {
+    Optional<String> valString = error ? condition.getErrorThreshold() : condition.getWarningThreshold();
+    return valString.map(s -> {
+      try {
+        switch (valueType) {
+          case BOOL:
+            return parseInteger(s) == 1;
+          case INT:
+          case RATING:
+            return parseInteger(s);
+          case MILLISEC:
+          case WORK_DUR:
+            return Long.parseLong(s);
+          case FLOAT:
+          case PERCENT:
+            return Double.parseDouble(s);
+          case STRING:
+          case LEVEL:
+            return s;
+          default:
+            throw new IllegalArgumentException(String.format("Unsupported value type %s. Cannot convert condition value", valueType));
+        }
+      } catch (NumberFormatException badValueFormat) {
+        throw new IllegalArgumentException(String.format(
+          "Quality Gate: unable to parse threshold '%s' to compare against %s", s, condition.getMetricKey()));
+      }
+    });
+  }
+
+  private static Optional<Comparable> getMeasureValue(Condition condition, QualityGateEvaluator.Measure measure) {
+    if (condition.isOnLeakPeriod()) {
+      return Optional.ofNullable(getLeakValue(measure));
+    }
+
+    return Optional.ofNullable(getValue(measure));
+  }
+
+  @CheckForNull
+  private static Comparable getValue(QualityGateEvaluator.Measure measure) {
+    if (NUMERICAL_TYPES.contains(measure.getType())) {
+      return measure.getValue().isPresent() ? getNumericValue(measure.getType(), measure.getValue().getAsDouble()) : null;
+    }
+
+    switch (measure.getType()) {
+      case LEVEL:
+      case STRING:
+      case DISTRIB:
+        return measure.getStringValue().orElse(null);
+      default:
+        throw new IllegalArgumentException("Condition on leak period is not allowed for type " + measure.getType());
+    }
+  }
+
+  @CheckForNull
+  private static Comparable getLeakValue(QualityGateEvaluator.Measure measure) {
+    if (NUMERICAL_TYPES.contains(measure.getType())) {
+      return measure.getLeakValue().isPresent() ? getNumericValue(measure.getType(), measure.getLeakValue().getAsDouble()) : null;
+    }
+
+    throw new IllegalArgumentException("Condition on leak period is not allowed for type " + measure.getType());
+  }
+
+  private static Comparable getNumericValue(ValueType type, double value) {
+    switch (type) {
+      case BOOL:
+        return Double.compare(value, 1.0) == 1;
+      case INT:
+      case RATING:
+        return (int) value;
+      case FLOAT:
+      case PERCENT:
+        return value;
+      case MILLISEC:
+      case WORK_DUR:
+        return (long) value;
+      default:
+        throw new IllegalArgumentException("Condition on numeric value is not allowed for type " + type);
+    }
+  }
+
+  private static int parseInteger(String value) {
+    return value.contains(".") ? Integer.parseInt(value.substring(0, value.indexOf('.'))) : Integer.parseInt(value);
+  }
+
+  private static boolean reachThreshold(Comparable measureValue, Comparable threshold, Condition condition) {
+    int comparison = measureValue.compareTo(threshold);
+    switch (condition.getOperator()) {
+      case EQUALS:
+        return comparison == 0;
+      case NOT_EQUALS:
+        return comparison != 0;
+      case GREATER_THAN:
+        return comparison > 0;
+      case LESS_THAN:
+        return comparison < 0;
+      default:
+        throw new IllegalArgumentException(String.format("Unsupported operator '%s'", condition.getOperator()));
+    }
+  }
+}
index 97aa900139ea49e122f820bb2715dd0f389f1d15..b47c176ac92de5aa9aa76a90e773c6de6a84bf7d 100644 (file)
@@ -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 + '\'')) +
       '}';
   }
 
index e262edcfb3b84f42cca834d50f95cd723d6bef0d..f568d242c762d4a925cb4c1c7f89919f1b20b6f9 100644 (file)
@@ -27,6 +27,7 @@ import java.util.Set;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
+import org.sonar.api.measures.Metric;
 import org.sonar.server.qualitygate.EvaluatedCondition.EvaluationStatus;
 
 import static com.google.common.base.Preconditions.checkArgument;
@@ -35,20 +36,22 @@ import static java.util.Objects.requireNonNull;
 @Immutable
 public class EvaluatedQualityGate {
   private final QualityGate qualityGate;
-  private final Status status;
+  private final Metric.Level status;
   private final Set<EvaluatedCondition> evaluatedConditions;
+  private final boolean ignoredConditionsOnSmallChangeset;
 
-  private EvaluatedQualityGate(QualityGate qualityGate, Status status, Set<EvaluatedCondition> evaluatedConditions) {
-    this.qualityGate = qualityGate;
-    this.status = status;
+  private EvaluatedQualityGate(QualityGate qualityGate, Metric.Level status, Set<EvaluatedCondition> evaluatedConditions, boolean ignoredConditionsOnSmallChangeset) {
+    this.qualityGate = requireNonNull(qualityGate, "qualityGate can't be null");
+    this.status = requireNonNull(status, "status can't be null");
     this.evaluatedConditions = evaluatedConditions;
+    this.ignoredConditionsOnSmallChangeset = ignoredConditionsOnSmallChangeset;
   }
 
   public QualityGate getQualityGate() {
     return qualityGate;
   }
 
-  public Status getStatus() {
+  public Metric.Level getStatus() {
     return status;
   }
 
@@ -56,6 +59,10 @@ public class EvaluatedQualityGate {
     return evaluatedConditions;
   }
 
+  public boolean hasIgnoredConditionsOnSmallChangeset() {
+    return ignoredConditionsOnSmallChangeset;
+  }
+
   public static Builder newBuilder() {
     return new Builder();
   }
@@ -90,20 +97,26 @@ public class EvaluatedQualityGate {
 
   public static final class Builder {
     private QualityGate qualityGate;
-    private Status status;
+    private Metric.Level status;
     private final Map<Condition, EvaluatedCondition> evaluatedConditions = new HashMap<>();
+    private boolean ignoredConditionsOnSmallChangeset = false;
 
     private Builder() {
       // use static factory method
     }
 
     public Builder setQualityGate(QualityGate qualityGate) {
-      this.qualityGate = checkQualityGate(qualityGate);
+      this.qualityGate = qualityGate;
+      return this;
+    }
+
+    public Builder setStatus(Metric.Level status) {
+      this.status = status;
       return this;
     }
 
-    public Builder setStatus(Status status) {
-      this.status = checkStatus(status);
+    public Builder setIgnoredConditionsOnSmallChangeset(boolean b) {
+      this.ignoredConditionsOnSmallChangeset = b;
       return this;
     }
 
@@ -112,18 +125,21 @@ public class EvaluatedQualityGate {
       return this;
     }
 
+    public Builder addCondition(EvaluatedCondition c) {
+      evaluatedConditions.put(c.getCondition(), c);
+      return this;
+    }
+
     public Set<EvaluatedCondition> getEvaluatedConditions() {
       return ImmutableSet.copyOf(evaluatedConditions.values());
     }
 
     public EvaluatedQualityGate build() {
-      checkQualityGate(this.qualityGate);
-      checkStatus(this.status);
-
       return new EvaluatedQualityGate(
         this.qualityGate,
         this.status,
-        checkEvaluatedConditions(qualityGate, evaluatedConditions));
+        checkEvaluatedConditions(qualityGate, evaluatedConditions),
+        ignoredConditionsOnSmallChangeset);
     }
 
     private static Set<EvaluatedCondition> checkEvaluatedConditions(QualityGate qualityGate, Map<Condition, EvaluatedCondition> evaluatedConditions) {
@@ -141,19 +157,5 @@ public class EvaluatedQualityGate {
 
       return ImmutableSet.copyOf(evaluatedConditions.values());
     }
-
-    private static QualityGate checkQualityGate(@Nullable QualityGate qualityGate) {
-      return requireNonNull(qualityGate, "qualityGate can't be null");
-    }
-
-    private static Status checkStatus(@Nullable Status status) {
-      return requireNonNull(status, "status can't be null");
-    }
-  }
-
-  public enum Status {
-    OK,
-    WARN,
-    ERROR
   }
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualitygate/LiveQualityGateFactory.java b/server/sonar-server/src/main/java/org/sonar/server/qualitygate/LiveQualityGateFactory.java
deleted file mode 100644 (file)
index c0638c8..0000000
+++ /dev/null
@@ -1,26 +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 org.sonar.db.component.ComponentDto;
-
-public interface LiveQualityGateFactory {
-  EvaluatedQualityGate buildForShortLivedBranch(ComponentDto componentDto);
-}
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 (file)
index 5598272..0000000
+++ /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<String, Long> typeFacet = new Facets(searchResponse, system2.getDefaultTimeZone())
-        .get(RuleIndex.FACET_TYPES);
-
-    EvaluatedQualityGate.Builder builder = EvaluatedQualityGate.newBuilder();
-    Set<Condition> conditions = ShortLivingBranchQualityGate.CONDITIONS.stream()
-        .map(c -> {
-          long measure = getMeasure(typeFacet, c);
-          EvaluatedCondition.EvaluationStatus status = measure > 0 ? EvaluatedCondition.EvaluationStatus.ERROR : EvaluatedCondition.EvaluationStatus.OK;
-          Condition condition = new Condition(c.getMetricKey(), toOperator(c), c.getErrorThreshold(), c.getWarnThreshold(), c.isOnLeak());
-          builder.addCondition(condition, status, valueOf(measure));
-          return condition;
-        })
-        .collect(toSet(ShortLivingBranchQualityGate.CONDITIONS.size()));
-    builder
-        .setQualityGate(
-            new org.sonar.server.qualitygate.QualityGate(
-                valueOf(ShortLivingBranchQualityGate.ID),
-                ShortLivingBranchQualityGate.NAME,
-                conditions))
-        .setStatus(qgStatusFrom(builder.getEvaluatedConditions()));
-
-    return builder.build();
-  }
-
-  private static Condition.Operator toOperator(ShortLivingBranchQualityGate.Condition c) {
-    String operator = c.getOperator();
-    switch (operator) {
-      case QualityGateConditionDto.OPERATOR_GREATER_THAN:
-        return Condition.Operator.GREATER_THAN;
-      case QualityGateConditionDto.OPERATOR_LESS_THAN:
-        return Condition.Operator.LESS_THAN;
-      case QualityGateConditionDto.OPERATOR_EQUALS:
-        return Condition.Operator.EQUALS;
-      case QualityGateConditionDto.OPERATOR_NOT_EQUALS:
-        return Condition.Operator.NOT_EQUALS;
-      default:
-        throw new IllegalArgumentException(format("Unsupported Condition operator '%s'", operator));
-    }
-  }
-
-  private static EvaluatedQualityGate.Status qgStatusFrom(Set<EvaluatedCondition> conditions) {
-    if (conditions.stream().anyMatch(c -> c.getStatus() == EvaluatedCondition.EvaluationStatus.ERROR)) {
-      return EvaluatedQualityGate.Status.ERROR;
-    }
-    return EvaluatedQualityGate.Status.OK;
-  }
-
-  private static long getMeasure(LinkedHashMap<String, Long> typeFacet, ShortLivingBranchQualityGate.Condition c) {
-    String metricKey = c.getMetricKey();
-    switch (metricKey) {
-      case CoreMetrics.BUGS_KEY:
-        return getValueForRuleType(typeFacet, RuleType.BUG);
-      case CoreMetrics.VULNERABILITIES_KEY:
-        return getValueForRuleType(typeFacet, RuleType.VULNERABILITY);
-      case CoreMetrics.CODE_SMELLS_KEY:
-        return getValueForRuleType(typeFacet, RuleType.CODE_SMELL);
-      default:
-        throw new IllegalArgumentException(format("Unsupported metric key '%s' in hardcoded quality gate", metricKey));
-    }
-  }
-
-  private static long getValueForRuleType(Map<String, Long> facet, RuleType ruleType) {
-    Long res = facet.get(ruleType.name());
-    if (res == null) {
-      return 0L;
-    }
-    return res;
-  }
-}
index a269d519ad291e394b23d094260453616432cc05..9d56caabfb73bd2b168b73548f69fcf73bb279de 100644 (file)
@@ -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 (file)
index 0000000..785ac31
--- /dev/null
@@ -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 (file)
index 0000000..3ecf03d
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.qualitygate;
+
+import java.util.Optional;
+import java.util.OptionalDouble;
+import java.util.Set;
+import org.sonar.api.ce.ComputeEngineSide;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.server.ServerSide;
+
+@ComputeEngineSide
+@ServerSide
+public interface QualityGateEvaluator {
+
+  /**
+   * @param measures must provide the measures related to the metrics
+   *                 defined by {@link #getMetricKeys(QualityGate)}
+   */
+  EvaluatedQualityGate evaluate(QualityGate gate, Measures measures);
+
+  /**
+   * Keys of the metrics involved in the computation of gate status.
+   * It may include metrics that are not part of conditions,
+   * for instance "new_lines" for the circuit-breaker on
+   * small changesets.
+   */
+  Set<String> getMetricKeys(QualityGate gate);
+
+  interface Measures {
+    Optional<Measure> get(String metricKey);
+  }
+
+  interface Measure {
+    Metric.ValueType getType();
+
+    OptionalDouble getValue();
+
+    Optional<String> getStringValue();
+
+    OptionalDouble getLeakValue();
+  }
+}
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 (file)
index 0000000..f0ce944
--- /dev/null
@@ -0,0 +1,133 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.qualitygate;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Multimap;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Metric.Level;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.server.qualitygate.EvaluatedCondition.EvaluationStatus;
+
+import static java.util.Objects.requireNonNull;
+import static org.sonar.core.util.stream.MoreCollectors.toEnumSet;
+
+public class QualityGateEvaluatorImpl implements QualityGateEvaluator {
+
+  private static final int MAXIMUM_NEW_LINES_FOR_SMALL_CHANGESETS = 20;
+  /**
+   * Some metrics will be ignored on very small change sets.
+   */
+  private static final Set<String> METRICS_TO_IGNORE_ON_SMALL_CHANGESETS = ImmutableSet.of(
+    CoreMetrics.NEW_COVERAGE_KEY,
+    CoreMetrics.NEW_LINE_COVERAGE_KEY,
+    CoreMetrics.NEW_BRANCH_COVERAGE_KEY,
+    CoreMetrics.NEW_DUPLICATED_LINES_DENSITY_KEY,
+    CoreMetrics.NEW_DUPLICATED_LINES_KEY,
+    CoreMetrics.NEW_BLOCKS_DUPLICATED_KEY);
+
+  @Override
+  public EvaluatedQualityGate evaluate(QualityGate gate, Measures measures) {
+    EvaluatedQualityGate.Builder result = EvaluatedQualityGate.newBuilder()
+      .setQualityGate(gate);
+
+    boolean isSmallChangeset = isSmallChangeset(measures);
+    Multimap<String, Condition> conditionsPerMetric = gate.getConditions().stream()
+      .collect(MoreCollectors.index(Condition::getMetricKey, Function.identity()));
+
+    for (Map.Entry<String, Collection<Condition>> entry : conditionsPerMetric.asMap().entrySet()) {
+      String metricKey = entry.getKey();
+      Collection<Condition> conditionsOnSameMetric = entry.getValue();
+
+      EvaluatedCondition evaluation = evaluateConditionsOnMetric(conditionsOnSameMetric, measures);
+
+      if (isSmallChangeset && evaluation.getStatus() != EvaluationStatus.OK && METRICS_TO_IGNORE_ON_SMALL_CHANGESETS.contains(metricKey)) {
+        result.addCondition(new EvaluatedCondition(evaluation.getCondition(), EvaluationStatus.OK, evaluation.getValue().orElse(null)));
+        result.setIgnoredConditionsOnSmallChangeset(true);
+      } else {
+        result.addCondition(evaluation);
+      }
+    }
+
+    result.setStatus(overallStatusOf(result.getEvaluatedConditions()));
+
+    return result.build();
+  }
+
+  @Override
+  public Set<String> getMetricKeys(QualityGate gate) {
+    Set<String> metricKeys = new HashSet<>();
+    metricKeys.add(CoreMetrics.NEW_LINES_KEY);
+    for (Condition condition : gate.getConditions()) {
+      metricKeys.add(condition.getMetricKey());
+    }
+    return metricKeys;
+  }
+
+  private static boolean isSmallChangeset(Measures measures) {
+    Optional<Measure> newLines = measures.get(CoreMetrics.NEW_LINES_KEY);
+    return newLines.isPresent() &&
+      newLines.get().getLeakValue().isPresent() &&
+      newLines.get().getLeakValue().getAsDouble() < MAXIMUM_NEW_LINES_FOR_SMALL_CHANGESETS;
+  }
+
+  private static EvaluatedCondition evaluateConditionsOnMetric(Collection<Condition> conditionsOnSameMetric, Measures measures) {
+    EvaluatedCondition leakEvaluation = null;
+    EvaluatedCondition absoluteEvaluation = null;
+    for (Condition condition : conditionsOnSameMetric) {
+      if (condition.isOnLeakPeriod()) {
+        leakEvaluation = ConditionEvaluator.evaluate(condition, measures);
+      } else {
+        absoluteEvaluation = ConditionEvaluator.evaluate(condition, measures);
+      }
+    }
+
+    if (leakEvaluation == null) {
+      return requireNonNull(absoluteEvaluation, "Evaluation of absolute value can't be null on conditions " + conditionsOnSameMetric);
+    }
+    if (absoluteEvaluation == null) {
+      return requireNonNull(leakEvaluation, "Evaluation of leak value can't be null on conditions " + conditionsOnSameMetric);
+    }
+    // both conditions are present. Take the worse one. In case of equality, take
+    // the one on the leak period
+    if (absoluteEvaluation.getStatus().compareTo(leakEvaluation.getStatus()) > 0) {
+      return absoluteEvaluation;
+    }
+    return leakEvaluation;
+  }
+
+  private static Level overallStatusOf(Set<EvaluatedCondition> conditions) {
+    Set<EvaluationStatus> statuses = conditions.stream().map(EvaluatedCondition::getStatus).collect(toEnumSet(EvaluationStatus.class));
+    if (statuses.contains(EvaluationStatus.ERROR)) {
+      return Level.ERROR;
+    }
+    if (statuses.contains(EvaluationStatus.WARN)) {
+      return Level.WARN;
+    }
+    return Level.OK;
+  }
+
+}
index 68dc868d3ef029e5f21dd38bb59321658dc351ad..d1abced201ed5ee35aea4c37e633d27e3a4abc57 100644 (file)
@@ -22,6 +22,7 @@ package org.sonar.server.qualitygate;
 import java.util.Optional;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
 import org.sonar.db.organization.OrganizationDto;
 import org.sonar.db.qualitygate.QGateWithOrgDto;
 import org.sonar.db.qualitygate.QualityGateDto;
@@ -44,16 +45,15 @@ public class QualityGateFinder {
    *
    * It will first try to get the quality gate explicitly defined on a project, if none it will try to return default quality gate ofI the organization
    */
-  public QualityGateData getQualityGate(DbSession dbSession, OrganizationDto organization, long componentId) {
-    Optional<Long> qualityGateId = dbClient.projectQgateAssociationDao().selectQGateIdByComponentId(dbSession, componentId);
+  public QualityGateData getQualityGate(DbSession dbSession, OrganizationDto organization, ComponentDto component) {
+    Optional<Long> qualityGateId = dbClient.projectQgateAssociationDao().selectQGateIdByComponentId(dbSession, component.getId());
     if (qualityGateId.isPresent()) {
       QualityGateDto qualityGate = checkFound(dbClient.qualityGateDao().selectById(dbSession, qualityGateId.get()), "No quality gate has been found for id %s", qualityGateId);
       return new QualityGateData(qualityGate, false);
-    } else {
-      QualityGateDto defaultQualityGate = dbClient.qualityGateDao().selectByOrganizationAndUuid(dbSession, organization, organization.getDefaultQualityGateUuid());
-      checkState(defaultQualityGate != null, "Unable to find the quality gate [%s] for organization [%s]", organization.getDefaultQualityGateUuid(), organization.getUuid());
-      return new QualityGateData(defaultQualityGate, true);
     }
+    QualityGateDto defaultQualityGate = dbClient.qualityGateDao().selectByOrganizationAndUuid(dbSession, organization, organization.getDefaultQualityGateUuid());
+    checkState(defaultQualityGate != null, "Unable to find the quality gate [%s] for organization [%s]", organization.getDefaultQualityGateUuid(), organization.getUuid());
+    return new QualityGateData(defaultQualityGate, true);
   }
 
   public QGateWithOrgDto getByOrganizationAndId(DbSession dbSession, OrganizationDto organization, long qualityGateId) {
index 6a7626518b048f4a662807cb527168a3dc298cdb..46f48b32c98a5016a1d33c347e184f41c46c34f0 100644 (file)
@@ -45,6 +45,7 @@ public class QualityGateModule extends Module {
       QualityGateUpdater.class,
       QualityGateConditionsUpdater.class,
       QualityGateFinder.class,
+      QualityGateEvaluatorImpl.class,
       // WS
       QualityGatesWsSupport.class,
       QualityGatesWs.class,
index 9cc85232103695086044587bc6ece2d158715ee9..3effb8aa841a6aae9c6e5af03c812052ff16ec7c 100644 (file)
@@ -39,7 +39,7 @@ import org.sonar.db.qualitygate.QualityGateConditionDao;
 import org.sonar.db.qualitygate.QualityGateConditionDto;
 import org.sonar.db.qualitygate.QualityGateDao;
 import org.sonar.db.qualitygate.QualityGateDto;
-import org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid;
+import org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating;
 
 import static java.util.Arrays.asList;
 import static java.util.stream.Collectors.toMap;
@@ -57,7 +57,7 @@ public class RegisterQualityGates implements Startable {
 
   private static final String BUILTIN_QUALITY_GATE_NAME = "Sonar way";
   private static final int LEAK_PERIOD = 1;
-  private static final String A_RATING = Integer.toString(RatingGrid.Rating.A.getIndex());
+  private static final String A_RATING = Integer.toString(Rating.A.getIndex());
 
   private static final List<QualityGateCondition> QUALITY_GATE_CONDITIONS = asList(
     new QualityGateCondition().setMetricKey(NEW_SECURITY_RATING_KEY).setOperator(OPERATOR_GREATER_THAN).setPeriod(LEAK_PERIOD).setErrorThreshold(A_RATING),
index 336568df8eb63203766633477bc436f1ce638b42..b086408aa9237c955cfd882c1a3d8b97067ed2c5 100644 (file)
@@ -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 (file)
index 0072045..0000000
+++ /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<QGChangeEvent> from(IssueChangeData issueChangeData, IssueChange issueChange, IssueChangeContext context);
-
-  final class IssueChange {
-    private final RuleType ruleType;
-    private final String transitionKey;
-
-    public IssueChange(RuleType ruleType) {
-      this(ruleType, null);
-    }
-
-    public IssueChange(String transitionKey) {
-      this(null, transitionKey);
-    }
-
-    public IssueChange(@Nullable RuleType ruleType, @Nullable String transitionKey) {
-      checkArgument(ruleType != null || transitionKey != null, "At least one of ruleType and transitionKey must be non null");
-      this.ruleType = ruleType;
-      this.transitionKey = transitionKey;
-    }
-
-    public Optional<RuleType> getRuleType() {
-      return Optional.ofNullable(ruleType);
-    }
-
-    public Optional<String> getTransitionKey() {
-      return Optional.ofNullable(transitionKey);
-    }
-
-    @Override
-    public boolean equals(Object o) {
-      if (this == o) {
-        return true;
-      }
-      if (o == null || getClass() != o.getClass()) {
-        return false;
-      }
-      IssueChange that = (IssueChange) o;
-      return ruleType == that.ruleType &&
-        Objects.equals(transitionKey, that.transitionKey);
-    }
-
-    @Override
-    public int hashCode() {
-      return Objects.hash(ruleType, transitionKey);
-    }
-
-    @Override
-    public String toString() {
-      return "IssueChange{" +
-        "ruleType=" + ruleType +
-        ", transitionKey='" + transitionKey + '\'' +
-        '}';
-    }
-  }
-
-  final class IssueChangeData {
-    private final List<DefaultIssue> issues;
-    private final List<ComponentDto> components;
-
-    public IssueChangeData(List<DefaultIssue> issues, List<ComponentDto> components) {
-      this.issues = ImmutableList.copyOf(issues);
-      this.components = ImmutableList.copyOf(components);
-    }
-
-    public List<DefaultIssue> getIssues() {
-      return issues;
-    }
-
-    public List<ComponentDto> getComponents() {
-      return components;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-      if (this == o) {
-        return true;
-      }
-      if (o == null || getClass() != o.getClass()) {
-        return false;
-      }
-      IssueChangeData that = (IssueChangeData) o;
-      return Objects.equals(issues, that.issues) &&
-        Objects.equals(components, that.components);
-    }
-
-    @Override
-    public int hashCode() {
-      return Objects.hash(issues, components);
-    }
-
-    @Override
-    public String toString() {
-      return "IssueChangeData{" +
-        "issues=" + issues +
-        ", components=" + components +
-        '}';
-    }
-  }
-}
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 (file)
index b1f3263..0000000
+++ /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<String> MEANINGFUL_TRANSITIONS = ImmutableSet.of(
-    DefaultTransitions.RESOLVE, DefaultTransitions.FALSE_POSITIVE, DefaultTransitions.WONT_FIX, DefaultTransitions.REOPEN);
-  private final DbClient dbClient;
-  private final ProjectConfigurationLoader projectConfigurationLoader;
-  private final LiveQualityGateFactory liveQualityGateFactory;
-
-  public QGChangeEventFactoryImpl(DbClient dbClient, ProjectConfigurationLoader projectConfigurationLoader,
-    LiveQualityGateFactory liveQualityGateFactory) {
-    this.dbClient = dbClient;
-    this.projectConfigurationLoader = projectConfigurationLoader;
-    this.liveQualityGateFactory = liveQualityGateFactory;
-  }
-
-  @Override
-  public List<QGChangeEvent> from(IssueChangeData issueChangeData, IssueChange issueChange, IssueChangeContext context) {
-    if (isEmpty(issueChangeData) || !isUserChangeContext(context) || !isRelevant(issueChange)) {
-      return emptyList();
-    }
-
-    return from(issueChangeData);
-  }
-
-  private static boolean isRelevant(IssueChange issueChange) {
-    return issueChange.getTransitionKey().map(QGChangeEventFactoryImpl::isMeaningfulTransition).orElse(true);
-  }
-
-  private static boolean isEmpty(IssueChangeData issueChangeData) {
-    return issueChangeData.getIssues().isEmpty();
-  }
-
-  private static boolean isUserChangeContext(IssueChangeContext context) {
-    return context.login() != null;
-  }
-
-  private static boolean isMeaningfulTransition(String transitionKey) {
-    return MEANINGFUL_TRANSITIONS.contains(transitionKey);
-  }
-
-  private List<QGChangeEvent> from(IssueChangeData issueChangeData) {
-    try (DbSession dbSession = dbClient.openSession(false)) {
-      Map<String, ComponentDto> branchesByUuid = getBranchComponents(dbSession, issueChangeData);
-      if (branchesByUuid.isEmpty()) {
-        return emptyList();
-      }
-
-      Set<String> branchProjectUuids = branchesByUuid.values().stream()
-        .map(ComponentDto::uuid)
-        .collect(toSet(branchesByUuid.size()));
-      Set<BranchDto> shortBranches = dbClient.branchDao().selectByUuids(dbSession, branchProjectUuids)
-        .stream()
-        .filter(branchDto -> branchDto.getBranchType() == BranchType.SHORT)
-        .collect(toSet(branchesByUuid.size()));
-      if (shortBranches.isEmpty()) {
-        return emptyList();
-      }
-
-      Map<String, Configuration> configurationByUuid = projectConfigurationLoader.loadProjectConfigurations(dbSession,
-        shortBranches.stream().map(shortBranch -> branchesByUuid.get(shortBranch.getUuid())).collect(Collectors.toSet()));
-      Set<String> shortBranchesComponentUuids = shortBranches.stream().map(BranchDto::getUuid).collect(toSet(shortBranches.size()));
-      Map<String, SnapshotDto> analysisByProjectUuid = dbClient.snapshotDao().selectLastAnalysesByRootComponentUuids(
-        dbSession,
-        shortBranchesComponentUuids)
-        .stream()
-        .collect(uniqueIndex(SnapshotDto::getComponentUuid));
-
-      return shortBranches
-        .stream()
-        .map(shortBranch -> {
-          ComponentDto branch = branchesByUuid.get(shortBranch.getUuid());
-          SnapshotDto analysis = analysisByProjectUuid.get(shortBranch.getUuid());
-          if (branch != null && analysis != null) {
-            Configuration configuration = configurationByUuid.get(shortBranch.getUuid());
-
-            return new QGChangeEvent(branch, shortBranch, analysis, configuration,
-              () -> Optional.of(liveQualityGateFactory.buildForShortLivedBranch(branch)));
-          }
-          return null;
-        })
-        .filter(Objects::nonNull)
-        .collect(MoreCollectors.toList(shortBranches.size()));
-    }
-  }
-
-  private Map<String, ComponentDto> getBranchComponents(DbSession dbSession, IssueChangeData issueChangeData) {
-    Set<String> projectUuids = issueChangeData.getIssues().stream()
-      .map(DefaultIssue::projectUuid)
-      .collect(toSet());
-    Set<String> missingProjectUuids = ImmutableSet.copyOf(Sets.difference(
-      projectUuids,
-      issueChangeData.getComponents()
-        .stream()
-        .map(ComponentDto::uuid)
-        .collect(Collectors.toSet())));
-    if (missingProjectUuids.isEmpty()) {
-      return issueChangeData.getComponents()
-        .stream()
-        .filter(c -> projectUuids.contains(c.uuid()))
-        .filter(componentDto -> componentDto.getMainBranchProjectUuid() != null)
-        .collect(uniqueIndex(ComponentDto::uuid));
-    }
-    return Stream.concat(
-      issueChangeData.getComponents().stream().filter(c -> projectUuids.contains(c.uuid())),
-      dbClient.componentDao().selectByUuids(dbSession, missingProjectUuids).stream())
-      .filter(componentDto -> componentDto.getMainBranchProjectUuid() != null)
-      .collect(uniqueIndex(ComponentDto::uuid));
-  }
-}
index 791164c7cd9ee2c846e8125f62b11c52e1dda30b..46921a783480507efa24347e08c42d0f55ffa29d 100644 (file)
 package org.sonar.server.qualitygate.changeevent;
 
 import java.util.Collection;
+import java.util.List;
+import org.sonar.core.issue.DefaultIssue;
 
 public interface QGChangeEventListeners {
 
-  void broadcastOnIssueChange(QGChangeEventFactory.IssueChangeData issueChangeData, Collection<QGChangeEvent> qgChangeEvents);
+  void broadcastOnIssueChange(List<DefaultIssue> changedIssues, Collection<QGChangeEvent> qgChangeEvents);
 }
index f471916f41cdf8b509544024e94ce13709e54b3d..b2fe05c836fceae3e52af4b7ccdbfb15e4fd6212 100644 (file)
@@ -22,6 +22,7 @@ package org.sonar.server.qualitygate.changeevent;
 import com.google.common.collect.Multimap;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.List;
 import java.util.Set;
 import org.sonar.api.rules.RuleType;
 import org.sonar.api.utils.log.Logger;
@@ -57,15 +58,15 @@ public class QGChangeEventListenersImpl implements QGChangeEventListeners {
   }
 
   @Override
-  public void broadcastOnIssueChange(QGChangeEventFactory.IssueChangeData issueChangeData, Collection<QGChangeEvent> changeEvents) {
-    if (listeners.length == 0 || issueChangeData.getComponents().isEmpty() || issueChangeData.getIssues().isEmpty() || changeEvents.isEmpty()) {
+  public void broadcastOnIssueChange(List<DefaultIssue> issues, Collection<QGChangeEvent> changeEvents) {
+    if (listeners.length == 0 || issues.isEmpty() || changeEvents.isEmpty()) {
       return;
     }
 
     try {
       Multimap<String, QGChangeEvent> eventsByComponentUuid = changeEvents.stream()
         .collect(MoreCollectors.index(t -> t.getProject().uuid()));
-      Multimap<String, DefaultIssue> issueByComponentUuid = issueChangeData.getIssues().stream()
+      Multimap<String, DefaultIssue> issueByComponentUuid = issues.stream()
         .collect(MoreCollectors.index(DefaultIssue::projectUuid));
 
       issueByComponentUuid.asMap()
index c5d07861053bdb61e2dc3e8b5ea1e0d1f06bceee..5b78ed31c9fe5e6143a232b2955b6d45dd60a619 100644 (file)
@@ -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);
     }
index 356eb87c43bbfdd7afa1f9409376c6cd3c72a0f5..32123e9ac059ec164bcf7620de586cb627e467ee 100644 (file)
  */
 package org.sonar.server.settings;
 
+import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
 import org.sonar.api.config.Configuration;
 import org.sonar.db.DbSession;
 import org.sonar.db.component.ComponentDto;
 
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+
 public interface ProjectConfigurationLoader {
   /**
    * Loads configuration for the specified components.
@@ -37,4 +41,9 @@ public interface ProjectConfigurationLoader {
    * Any component is accepted but SQ only supports specific properties for projects and branches.
    */
   Map<String, Configuration> loadProjectConfigurations(DbSession dbSession, Set<ComponentDto> projects);
+
+  default Configuration loadProjectConfiguration(DbSession dbSession, ComponentDto project) {
+    Map<String, Configuration> configurations = loadProjectConfigurations(dbSession, Collections.singleton(project));
+    return requireNonNull(configurations.get(project.uuid()), () -> format("Configuration for project '%s' is not found", project.getKey()));
+  }
 }
index 5c96aa6479d451301604622f3c5a76c5cb8bb490..01e8bcbaa08c38e66a2c5ceb91b2a21c1098ba0d 100644 (file)
@@ -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:
index 79ea6d75af93762f304c366ba27f50baea452903..580a62cd94abc5a40e9216315e10ae135177bd90 100644 (file)
@@ -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())
index 5f1015685835e7321130e442f5792653b4d9a793..85c1a460e9554dc1d4e48f27bcaebbaddd5fa11a 100644 (file)
@@ -22,6 +22,7 @@ package org.sonar.server.webhook;
 import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
+import org.apache.commons.lang.StringUtils;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.component.AnalysisPropertyDto;
@@ -30,6 +31,7 @@ import org.sonar.db.component.ComponentDto;
 import org.sonar.db.component.SnapshotDto;
 import org.sonar.server.qualitygate.changeevent.QGChangeEvent;
 import org.sonar.server.qualitygate.changeevent.QGChangeEventListener;
+import org.sonar.server.webhook.Branch.Type;
 
 public class WebhookQGChangeEventListener implements QGChangeEventListener {
   private final WebHooks webhooks;
@@ -61,17 +63,18 @@ public class WebhookQGChangeEventListener implements QGChangeEventListener {
   }
 
   private WebhookPayload buildWebHookPayload(DbSession dbSession, QGChangeEvent event) {
-    ComponentDto branch = event.getProject();
-    BranchDto shortBranch = event.getBranch();
+    ComponentDto project = event.getProject();
+    BranchDto branch = event.getBranch();
     SnapshotDto analysis = event.getAnalysis();
     Map<String, String> analysisProperties = dbClient.analysisPropertiesDao().selectBySnapshotUuid(dbSession, analysis.getUuid())
       .stream()
       .collect(Collectors.toMap(AnalysisPropertyDto::getKey, AnalysisPropertyDto::getValue));
+    String projectUuid = StringUtils.defaultString(project.getMainBranchProjectUuid(), project.projectUuid());
     ProjectAnalysis projectAnalysis = new ProjectAnalysis(
-      new Project(branch.getMainBranchProjectUuid(), branch.getKey(), branch.name()),
+      new Project(projectUuid, project.getKey(), project.name()),
       null,
       new Analysis(analysis.getUuid(), analysis.getCreatedAt()),
-      new Branch(false, shortBranch.getKey(), Branch.Type.SHORT),
+      new Branch(branch.isMain(), branch.getKey(), Type.valueOf(branch.getBranchType().name())),
       event.getQualityGateSupplier().get().orElse(null),
       null,
       analysisProperties);
index 48b5b99bd700a0e36c1c898d2368d6c927fb88e4..8eab7f02c95c305db961a65d7420156832ad6c41 100644 (file)
@@ -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;
   }
 }
index a456e2b84ed1e31f3c176b172112f2bd814dbc87..742bb1d35e91d09cb4f421b1a181e46c4f5edbf2 100644 (file)
 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 {
 
index ac7f9efdfdc1343f9191fadf917e98f7f801353d..e023123bc952db07509bbd11b5ed3f23662c089d 100644 (file)
@@ -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 {
 
index 5db2a72cc8c0dbf76afb1aad8b064e3e8d47426b..cd1c045d9d37b3476b1333556671afdb481b994e 100644 (file)
@@ -19,8 +19,9 @@
  */
 package org.sonar.server.computation.task.projectanalysis.qualitygate;
 
-import com.google.common.base.Optional;
+import java.util.Optional;
 import org.junit.rules.ExternalResource;
+import org.sonar.server.qualitygate.EvaluatedQualityGate;
 
 public class MutableQualityGateHolderRule extends ExternalResource implements MutableQualityGateHolder {
   private MutableQualityGateHolder delegate = new QualityGateHolderImpl();
@@ -31,13 +32,18 @@ public class MutableQualityGateHolderRule extends ExternalResource implements Mu
   }
 
   @Override
-  public void setNoQualityGate() {
-    delegate.setNoQualityGate();
+  public Optional<QualityGate> getQualityGate() {
+    return delegate.getQualityGate();
   }
 
   @Override
-  public Optional<QualityGate> getQualityGate() {
-    return delegate.getQualityGate();
+  public void setEvaluation(EvaluatedQualityGate evaluation) {
+    delegate.setEvaluation(evaluation);
+  }
+
+  @Override
+  public Optional<EvaluatedQualityGate> getEvaluation() {
+    return delegate.getEvaluation();
   }
 
   @Override
index 24e6a1a7be3c0582c28906242196a390188b294d..a53912856e3dabc09b8fe0279c4eeb95d3d5cc03 100644 (file)
  */
 package org.sonar.server.computation.task.projectanalysis.qualitygate;
 
-import java.util.Collections;
 import org.junit.Test;
+import org.sonar.server.qualitygate.EvaluatedQualityGate;
 
+import static java.util.Collections.emptyList;
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.guava.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
 
 public class QualityGateHolderImplTest {
 
-  public static final QualityGate QUALITY_GATE = new QualityGate(4612, "name", Collections.<Condition>emptyList());
+  private static final QualityGate QUALITY_GATE = new QualityGate(4612, "name", emptyList());
 
   @Test(expected = IllegalStateException.class)
   public void getQualityGate_throws_ISE_if_QualityGate_not_set() {
@@ -56,13 +57,33 @@ public class QualityGateHolderImplTest {
     assertThat(holder.getQualityGate().get()).isSameAs(QUALITY_GATE);
   }
 
+  @Test(expected = IllegalStateException.class)
+  public void getEvaluation_throws_ISE_if_QualityGate_not_set() {
+    new QualityGateHolderImpl().getEvaluation();
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void setEvaluation_throws_NPE_if_argument_is_null() {
+    new QualityGateHolderImpl().setEvaluation(null);
+  }
+
+  @Test(expected = IllegalStateException.class)
+  public void setEvaluation_throws_ISE_if_called_twice() {
+    QualityGateHolderImpl holder = new QualityGateHolderImpl();
+
+    EvaluatedQualityGate evaluation = mock(EvaluatedQualityGate.class);
+    holder.setEvaluation(evaluation);
+    holder.setEvaluation(evaluation);
+  }
+
   @Test
-  public void getQualityGate_returns_absent_if_holder_initialized_with_setNoQualityGate() {
+  public void getEvaluation_returns_QualityGate_set_by_setQualityGate() {
     QualityGateHolderImpl holder = new QualityGateHolderImpl();
 
-    holder.setNoQualityGate();
+    EvaluatedQualityGate evaluation = mock(EvaluatedQualityGate.class);
+    holder.setEvaluation(evaluation);
 
-    assertThat(holder.getQualityGate()).isAbsent();
+    assertThat(holder.getEvaluation().get()).isSameAs(evaluation);
   }
 
 }
index 16485fd5d1a0c18b07163fe86369f37f74a21348..10a0c320eb53eaa3d9bb8df0ecc8c35665cebbb3 100644 (file)
  */
 package org.sonar.server.computation.task.projectanalysis.qualitygate;
 
-import com.google.common.base.Optional;
+import java.util.Optional;
 import javax.annotation.Nullable;
 import org.junit.rules.ExternalResource;
+import org.sonar.server.qualitygate.EvaluatedQualityGate;
 
 import static com.google.common.base.Preconditions.checkState;
 
 public class QualityGateHolderRule extends ExternalResource implements QualityGateHolder {
+  @Nullable
   private Optional<QualityGate> qualityGate;
+  @Nullable
+  private Optional<EvaluatedQualityGate> evaluation;
 
   public void setQualityGate(@Nullable QualityGate qualityGate) {
-    this.qualityGate = Optional.fromNullable(qualityGate);
+    this.qualityGate = Optional.ofNullable(qualityGate);
   }
 
   @Override
@@ -38,6 +42,16 @@ public class QualityGateHolderRule extends ExternalResource implements QualityGa
     return qualityGate;
   }
 
+  public void setEvaluation(@Nullable EvaluatedQualityGate e) {
+    this.evaluation = Optional.ofNullable(e);
+  }
+
+  @Override
+  public Optional<EvaluatedQualityGate> getEvaluation() {
+    checkState(evaluation != null, "EvaluatedQualityGate has not been initialized");
+    return evaluation;
+  }
+
   @Override
   protected void after() {
     reset();
@@ -45,5 +59,6 @@ public class QualityGateHolderRule extends ExternalResource implements QualityGa
 
   public void reset() {
     this.qualityGate = null;
+    this.evaluation = null;
   }
 }
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/DebtRatingGridTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/DebtRatingGridTest.java
new file mode 100644 (file)
index 0000000..5966979
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.computation.task.projectanalysis.qualitymodel;
+
+import org.junit.Before;
+import org.junit.Rule;
+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.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 DebtRatingGridTest {
+
+  private DebtRatingGrid ratingGrid;
+
+  @Rule
+  public ExpectedException throwable = ExpectedException.none();
+
+  @Before
+  public void setUp() {
+    double[] gridValues = new double[] {0.1, 0.2, 0.5, 1};
+    ratingGrid = new DebtRatingGrid(gridValues);
+  }
+
+  @Test
+  public void return_rating_matching_density() {
+    assertThat(ratingGrid.getRatingForDensity(0)).isEqualTo(A);
+    assertThat(ratingGrid.getRatingForDensity(0.05)).isEqualTo(A);
+    assertThat(ratingGrid.getRatingForDensity(0.09999999)).isEqualTo(A);
+    assertThat(ratingGrid.getRatingForDensity(0.1)).isEqualTo(B);
+    assertThat(ratingGrid.getRatingForDensity(0.15)).isEqualTo(B);
+    assertThat(ratingGrid.getRatingForDensity(0.2)).isEqualTo(C);
+    assertThat(ratingGrid.getRatingForDensity(0.25)).isEqualTo(C);
+    assertThat(ratingGrid.getRatingForDensity(0.5)).isEqualTo(D);
+    assertThat(ratingGrid.getRatingForDensity(0.65)).isEqualTo(D);
+    assertThat(ratingGrid.getRatingForDensity(1)).isEqualTo(E);
+    assertThat(ratingGrid.getRatingForDensity(1.01)).isEqualTo(E);
+  }
+
+  @Test
+  public void fail_on_invalid_density() {
+    throwable.expect(RuntimeException.class);
+
+    ratingGrid.getRatingForDensity(-1);
+  }
+
+  @Test
+  public void convert_int_to_rating() throws Exception {
+    assertThat(Rating.valueOf(1)).isEqualTo(A);
+    assertThat(Rating.valueOf(2)).isEqualTo(B);
+    assertThat(Rating.valueOf(3)).isEqualTo(C);
+    assertThat(Rating.valueOf(4)).isEqualTo(D);
+    assertThat(Rating.valueOf(5)).isEqualTo(E);
+  }
+
+  @Test
+  public void fail_to_concert_invalid_value() throws Exception {
+    throwable.expect(IllegalArgumentException.class);
+    Rating.valueOf(10);
+  }
+}
index f3b31742c57cdaee0800257da9868c1d94eb8791..b77f3a0780bc6c637099c2313a490e5b5b26501d 100644 (file)
@@ -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);
 
index 8d0d84ee4a055519f466ccc3b1c6c9435f9ed3ac..c39cd3146db3e08d8872775d0ef9a889a28ac121 100644 (file)
@@ -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());
   }
index 89ad11a779eb3eea4fe60bcfbb0633a11cfdde20..51393a717e2e35e417c3dac136e37dc67098dc8d 100644 (file)
@@ -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/RatingGridTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingGridTest.java
deleted file mode 100644 (file)
index 7149a2e..0000000
+++ /dev/null
@@ -1,84 +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.computation.task.projectanalysis.qualitymodel;
-
-import org.junit.Before;
-import org.junit.Rule;
-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 {
-
-  private RatingGrid ratingGrid;
-
-  @Rule
-  public ExpectedException throwable = ExpectedException.none();
-
-  @Before
-  public void setUp() {
-    double[] gridValues = new double[] {0.1, 0.2, 0.5, 1};
-    ratingGrid = new RatingGrid(gridValues);
-  }
-
-  @Test
-  public void return_rating_matching_density() {
-    assertThat(ratingGrid.getRatingForDensity(0)).isEqualTo(A);
-    assertThat(ratingGrid.getRatingForDensity(0.05)).isEqualTo(A);
-    assertThat(ratingGrid.getRatingForDensity(0.09999999)).isEqualTo(A);
-    assertThat(ratingGrid.getRatingForDensity(0.1)).isEqualTo(B);
-    assertThat(ratingGrid.getRatingForDensity(0.15)).isEqualTo(B);
-    assertThat(ratingGrid.getRatingForDensity(0.2)).isEqualTo(C);
-    assertThat(ratingGrid.getRatingForDensity(0.25)).isEqualTo(C);
-    assertThat(ratingGrid.getRatingForDensity(0.5)).isEqualTo(D);
-    assertThat(ratingGrid.getRatingForDensity(0.65)).isEqualTo(D);
-    assertThat(ratingGrid.getRatingForDensity(1)).isEqualTo(E);
-    assertThat(ratingGrid.getRatingForDensity(1.01)).isEqualTo(E);
-  }
-
-  @Test
-  public void fail_on_invalid_density() {
-    throwable.expect(RuntimeException.class);
-
-    ratingGrid.getRatingForDensity(-1);
-  }
-
-  @Test
-  public void convert_int_to_rating() throws Exception {
-    assertThat(Rating.valueOf(1)).isEqualTo(A);
-    assertThat(Rating.valueOf(2)).isEqualTo(B);
-    assertThat(Rating.valueOf(3)).isEqualTo(C);
-    assertThat(Rating.valueOf(4)).isEqualTo(D);
-    assertThat(Rating.valueOf(5)).isEqualTo(E);
-  }
-
-  @Test
-  public void fail_to_concert_invalid_value() throws Exception {
-    throwable.expect(IllegalArgumentException.class);
-    Rating.valueOf(10);
-  }
-}
index 5bcbc3fb662e2ba669c2f0ae7ff923fef1f556d0..2fb023fa235222c10d42f14fa67ce32fb8050427 100644 (file)
  */
 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());
index bc445fb7ae3aacd018f1a9bc31643c9b70ad7191..0d0ac7d81023f3a58dc2e68426253ee16a5db3d0 100644 (file)
@@ -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 {
 
index 69a9f29b7c701cedbe82ff30e915279d2dbde943..016f1c1d751b7db481280b4310c349b0beed396a 100644 (file)
@@ -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();
-  }
-
 }
index d92e79d0861b13302658de70ddd3ee00aba9d60c..511e460d07def2d5f0340ac183a3d0ad774e790e 100644 (file)
@@ -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();
     }
index d7f50738fbb6b40a92cddb5cb76046dc30620161..d082951d143480cc8b3f329e5bca913883939a41 100644 (file)
@@ -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<String, Object> properties, Collection<DefaultIssue> issues, UserSession userSession) {
-          return false;
-        }
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Action key must be set");
 
-        @Override
-        public boolean execute(Map<String, Object> 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<String, Object> properties, Collection<DefaultIssue> 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<String, Object> properties, Collection<DefaultIssue> issues, UserSession userSession) {
+      return false;
+    }
+
+    @Override
+    public boolean execute(Map<String, Object> properties, Context context) {
+      return false;
+    }
 
-        @Override
-        public boolean execute(Map<String, Object> 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;
     }
   }
 }
index 8eb00e90541f60998a3d6155e7d7e5d24ec96005..f9f42cfc2d6131d2bbe307065087f7923e4029dc 100644 (file)
@@ -83,11 +83,12 @@ public class IssueUpdaterTest {
   private ArgumentCaptor<IssueChangeNotification> 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 (file)
index 0000000..e18cf41
--- /dev/null
@@ -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<ComponentDto> calledComponents = new ArrayList<>();
+
+  @Override
+  public void process(DbSession dbSession, List<DefaultIssue> changedIssues, Collection<ComponentDto> components) {
+    called = true;
+    calledComponents.addAll(components);
+  }
+
+  public boolean wasCalled() {
+    return called;
+  }
+
+  public List<ComponentDto> calledComponents() {
+    return calledComponents;
+  }
+}
index d731f9261152d43e43978a0189d87198b836ff46..67b52acfd0ca5ca2168d89ef958c3dedcb022bed 100644 (file)
@@ -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<SearchResponseData> 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);
 
index 6def10974743711e8067d8c1aefc7d8d1df71673..aed5de8eb823c7602b2d693b0504137b4c82793c 100644 (file)
@@ -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<SearchResponseData> 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);
index e75761f735131be5a4a03d273c4e6d2175444d20..764ebe56070b7a28a98a16c66a535f33c1c4f34f 100644 (file)
@@ -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<Action> 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<QGChangeEvent> 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<QGChangeEvent> 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<QGChangeEvent> 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<QGChangeEvent> 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<QGChangeEvent> 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<IssueChangeNotification> issueChangeNotificationCaptor = ArgumentCaptor.forClass(IssueChangeNotification.class);
-    List<QGChangeEvent> 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<QGChangeEvent> 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<QGChangeEvent> 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<QGChangeEvent> 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<QGChangeEvent> 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<QGChangeEvent> changeEvents = Collections.singletonList(mock(QGChangeEvent.class));
-    when(qgChangeEventFactory.from(any(QGChangeEventFactory.IssueChangeData.class), eq(issueChange), eq(user))).thenReturn(changeEvents);
-
-    ArgumentCaptor<QGChangeEventFactory.IssueChangeData> 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<QGChangeEvent> 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<QGChangeEvent> 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<QGChangeEventFactory.IssueChangeData> 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<QGChangeEvent> 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);
   }
index 42b9de089662f010471f78309193553da5ee2dd2..d3c69b8d6966d49d2d2bf7ce236aedc4a47672c0 100644 (file)
@@ -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<SearchResponseData> 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<QGChangeEvent> 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<QGChangeEventFactory.IssueChangeData> 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
index 267c06f989f654b670a42cffddf4b4108817126f..32bfa672ff4bfa8fef383ada166ddc4173a3d9a1 100644 (file)
@@ -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);
   }
 }
 
index 30d2a740ba74314b10028c064a7e6412e7c22461..545ce4d5ec72954c94a35a83c3025927719f9802 100644 (file)
@@ -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<SearchResponseData> 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);
 
index 5df04b4ee93eac54f255e0fac7c74a92efb7ea27..22e9634f98d94a1d205161228a345db3731add33 100644 (file)
@@ -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<SearchResponseData> 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
index c7edce5e1f50596b484d37ae3a8d0f5572b54541..e51d37abab2dee1a2c700e5878c9acc0e84e15f1 100644 (file)
@@ -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<SearchResponseData> 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<QGChangeEvent> 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<QGChangeEventFactory.IssueChangeData> 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 (file)
index 0000000..8e7d34e
--- /dev/null
@@ -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<Metric, Double> values = new HashMap<>();
+    private final Map<Metric, Double> 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<Metric> dependentMetrics;
+    private Double doubleValue;
+    private Rating ratingValue;
+    private Double doubleLeakValue;
+    private Rating ratingLeakValue;
+    private final Map<Metric, Double> values;
+    private final Map<Metric, Double> leakValues;
+
+    private TestContext(Collection<Metric> dependentMetrics, Map<Metric, Double> values, Map<Metric, Double> leakValues) {
+      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<Double> 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<Double> 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 (file)
index 0000000..be76582
--- /dev/null
@@ -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<QGChangeEvent> result = run(asList(file1, file2), newQualifierBasedIntFormula(), newRatingConstantFormula(Rating.C));
+
+    // 2 measures per component have been created
+    // Numeric value depends on qualifier (see newQualifierBasedIntFormula())
+    assertThat(db.countRowsOfTable(db.getSession(), "live_measures")).isEqualTo(8);
+    assertThatIntMeasureHasValue(file1, ORDERED_BOTTOM_UP.indexOf(Qualifiers.FILE));
+    assertThatRatingMeasureHasValue(file1, Rating.C);
+    assertThatIntMeasureHasValue(file2, ORDERED_BOTTOM_UP.indexOf(Qualifiers.FILE));
+    assertThatRatingMeasureHasValue(file2, Rating.C);
+    assertThatIntMeasureHasValue(dir, ORDERED_BOTTOM_UP.indexOf(Qualifiers.DIRECTORY));
+    assertThatRatingMeasureHasValue(dir, Rating.C);
+    assertThatIntMeasureHasValue(project, ORDERED_BOTTOM_UP.indexOf(Qualifiers.PROJECT));
+    assertThatRatingMeasureHasValue(project, Rating.C);
+    assertThatProjectChanged(result, project);
+  }
+
+  @Test
+  public void compute_and_update_measures_if_they_already_exist() {
+    markProjectAsAnalyzed(project);
+    db.measures().insertLiveMeasure(project, intMetric, m -> m.setValue(42.0));
+    db.measures().insertLiveMeasure(dir, intMetric, m -> m.setValue(42.0));
+    db.measures().insertLiveMeasure(file1, intMetric, m -> m.setValue(42.0));
+    db.measures().insertLiveMeasure(file2, intMetric, m -> m.setValue(42.0));
+
+    // generates values 1, 2, 3
+    List<QGChangeEvent> result = run(file1, newQualifierBasedIntFormula());
+
+    assertThat(db.countRowsOfTable(db.getSession(), "live_measures")).isEqualTo(4);
+    assertThatProjectChanged(result, project);
+
+    // Numeric value depends on qualifier (see newQualifierBasedIntFormula())
+    assertThatIntMeasureHasValue(file1, ORDERED_BOTTOM_UP.indexOf(Qualifiers.FILE));
+    assertThatIntMeasureHasValue(dir, ORDERED_BOTTOM_UP.indexOf(Qualifiers.DIRECTORY));
+    assertThatIntMeasureHasValue(project, ORDERED_BOTTOM_UP.indexOf(Qualifiers.PROJECT));
+    // untouched
+    assertThatIntMeasureHasValue(file2, 42.0);
+  }
+
+  @Test
+  public void variation_is_refreshed_when_int_value_is_changed() {
+    markProjectAsAnalyzed(project);
+    // value is:
+    // 42 on last analysis
+    // 42-12=30 on beginning of leak period
+    db.measures().insertLiveMeasure(project, intMetric, m -> m.setValue(42.0).setVariation(12.0));
+
+    // new value is 44, so variation on leak period is 44-30=14
+    List<QGChangeEvent> result = run(file1, newIntConstantFormula(44.0));
+
+    LiveMeasureDto measure = assertThatIntMeasureHasValue(project, 44.0);
+    assertThat(measure.getVariation()).isEqualTo(14.0);
+    assertThatProjectChanged(result, project);
+  }
+
+  @Test
+  public void variation_is_refreshed_when_rating_value_is_changed() {
+    markProjectAsAnalyzed(project);
+    // value is:
+    // B on last analysis
+    // D on beginning of leak period --> variation is -2
+    db.measures().insertLiveMeasure(project, ratingMetric, m -> m.setValue((double) Rating.B.getIndex()).setData("B").setVariation(-2.0));
+
+    // new value is C, so variation on leak period is D to C = -1
+    List<QGChangeEvent> result = run(file1, newRatingConstantFormula(Rating.C));
+
+    LiveMeasureDto measure = assertThatRatingMeasureHasValue(project, Rating.C);
+    assertThat(measure.getVariation()).isEqualTo(-1.0);
+    assertThatProjectChanged(result, project);
+  }
+
+  @Test
+  public void variation_does_not_change_if_rating_value_does_not_change() {
+    markProjectAsAnalyzed(project);
+    // value is:
+    // B on last analysis
+    // D on beginning of leak period --> variation is -2
+    db.measures().insertLiveMeasure(project, ratingMetric, m -> m.setValue((double) Rating.B.getIndex()).setData("B").setVariation(-2.0));
+
+    // new value is still B, so variation on leak period is still -2
+    List<QGChangeEvent> result = run(file1, newRatingConstantFormula(Rating.B));
+
+    LiveMeasureDto measure = assertThatRatingMeasureHasValue(project, Rating.B);
+    assertThat(measure.getVariation()).isEqualTo(-2.0);
+    assertThatProjectChanged(result, project);
+  }
+
+  @Test
+  public void refresh_leak_measures() {
+    markProjectAsAnalyzed(project);
+    db.measures().insertLiveMeasure(project, intMetric, m -> m.setVariation(42.0).setValue(null));
+    db.measures().insertLiveMeasure(project, ratingMetric, m -> m.setVariation((double)Rating.E.getIndex()));
+    db.measures().insertLiveMeasure(dir, intMetric, m -> m.setVariation(42.0).setValue(null));
+    db.measures().insertLiveMeasure(dir, ratingMetric, m -> m.setVariation((double)Rating.D.getIndex()));
+    db.measures().insertLiveMeasure(file1, intMetric, m -> m.setVariation(42.0).setValue(null));
+    db.measures().insertLiveMeasure(file1, ratingMetric, m -> m.setVariation((double)Rating.C.getIndex()));
+
+    // generates values 1, 2, 3 on leak measures
+    List<QGChangeEvent> result = run(file1, newQualifierBasedIntLeakFormula(), newRatingLeakFormula(Rating.B));
+
+    assertThat(db.countRowsOfTable(db.getSession(), "live_measures")).isEqualTo(6);
+
+    // Numeric value depends on qualifier (see newQualifierBasedIntLeakFormula())
+    assertThatIntMeasureHasLeakValue(file1, ORDERED_BOTTOM_UP.indexOf(Qualifiers.FILE));
+    assertThatRatingMeasureHasLeakValue(file1, Rating.B);
+    assertThatIntMeasureHasLeakValue(dir, ORDERED_BOTTOM_UP.indexOf(Qualifiers.DIRECTORY));
+    assertThatRatingMeasureHasLeakValue(dir, Rating.B);
+    assertThatIntMeasureHasLeakValue(project, ORDERED_BOTTOM_UP.indexOf(Qualifiers.PROJECT));
+    assertThatRatingMeasureHasLeakValue(project, Rating.B);
+    assertThatProjectChanged(result, project);
+  }
+
+  @Test
+  public void do_nothing_if_project_has_not_being_analyzed() {
+    // project has no snapshots
+    List<QGChangeEvent> 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<QGChangeEvent> 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<QGChangeEvent> result = run(asList(file1, fileInProject2), newQualifierBasedIntFormula());
+
+    // generated values depend on position of qualifier in Qualifiers.ORDERED_BOTTOM_UP (see formula)
+    assertThatIntMeasureHasValue(file1, 0);
+    assertThatIntMeasureHasValue(dir, 2);
+    assertThatIntMeasureHasValue(project, 4);
+    assertThatIntMeasureHasValue(fileInProject2, 0);
+    assertThatIntMeasureHasValue(project2, 4);
+
+    // no other measures generated
+    assertThat(db.countRowsOfTable(db.getSession(), "live_measures")).isEqualTo(5);
+    assertThatProjectChanged(result, project, project2);
+  }
+
+  @Test
+  public void refresh_multiple_branches_at_the_same_time() {
+    // FIXME
+  }
+
+  @Test
+  public void 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<QGChangeEvent> run(ComponentDto component, IssueMetricFormula... formulas) {
+    return run(singleton(component), formulas);
+  }
+
+  private List<QGChangeEvent> run(Collection<ComponentDto> components, IssueMetricFormula... formulas) {
+    IssueMetricFormulaFactory formulaFactory = new TestIssueMetricFormulaFactory(asList(formulas));
+
+    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<QGChangeEvent> 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<QGChangeEvent> 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 (file)
index 0000000..3ac75e0
--- /dev/null
@@ -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 (file)
index 0000000..945165d
--- /dev/null
@@ -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<String> 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<String> 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 (file)
index 0000000..a163dc2
--- /dev/null
@@ -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<MetricDto> 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<MetricDto> 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<LiveMeasureDto> 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 (file)
index 0000000..ee29a7e
--- /dev/null
@@ -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<IssueMetricFormula> formulas;
+
+  TestIssueMetricFormulaFactory(List<IssueMetricFormula> formulas) {
+    this.formulas = formulas;
+  }
+
+  @Override
+  public List<IssueMetricFormula> getFormulas() {
+    return formulas;
+  }
+
+  @Override
+  public Set<Metric> 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 (file)
index 0000000..f8b4234
--- /dev/null
@@ -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<QualityGateEvaluator.Measure> 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<String> 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);
+  // }
+}
index 9c55e05e952246a9cbf0ee6df972da035c1693f0..b02d5dd2cd5bf407d9d692919c279e2047d59f9a 100644 (file)
@@ -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 (file)
index 955d56b..0000000
+++ /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<String> OPEN_STATUSES = ImmutableList.of(Issue.STATUS_OPEN, Issue.STATUS_CONFIRMED);
-  private static final List<String> 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/test/java/org/sonar/server/qualitygate/QualityGateConverterTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualitygate/QualityGateConverterTest.java
new file mode 100644 (file)
index 0000000..c003126
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * 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 org.junit.Test;
+
+public class QualityGateConverterTest {
+
+  @Test
+  public void test_ToJson() {
+    // FIXME
+  }
+
+}
index b037cedc155ee910a9946e333e6c248961cac430..fc05daf97d43b2c1f5d1328e27274989837a999a 100644 (file)
@@ -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
index 686a0a543f150cd3a30657b93aac48b615372026..d0dd02019704bf795a0a5485544938288ca2110e 100644 (file)
@@ -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 (file)
index ae01892..0000000
+++ /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<String> 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<String> 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<String, String> properties = new HashMap<>();
-    properties.put("sonar.analysis.test1", randomAlphanumeric(50));
-    properties.put("sonar.analysis.test2", randomAlphanumeric(5000));
-    insertPropertiesFor(analysis.getUuid(), properties);
-
-    Collection<QGChangeEvent> 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<IssueDto> 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<IssueDto> 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<QGChangeEvent> 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<String> 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<QGChangeEvent> 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<String> 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<IssueDto> 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<QGChangeEvent> 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<IssueDto> 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<QGChangeEvent> qgChangeEvents = underTest.from(
-      issueChangeData(issueDtos, branch1, branch3),
-      new IssueChange(randomRuleType),
-      userChangeContext);
-
-    assertThat(qgChangeEvents).hasSize(3);
-
-    Set<String> 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<IssueDto> 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<QGChangeEvent> 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<String> 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<ComponentDto> components = new HashSet<>();
-    Map<String, Configuration> 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<String, String> properties) {
-    List<AnalysisPropertyDto> 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<IssueDto> 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 (file)
index 62d95d8..0000000
+++ /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)));
-  }
-}
index 8e729ebfe8cb9c8dacd22f8f08b0d52bae29cd37..47cee4474b71e735c90a0f696fb0a31f0b92dfb1 100644 (file)
@@ -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<DefaultIssue> 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<DefaultIssue> changedIssues = randomizedList(issues);
     List<QGChangeEvent> 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 (file)
index 0000000..f1a2527
--- /dev/null
@@ -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<String, Configuration> loadProjectConfigurations(DbSession dbSession, Set<ComponentDto> projects) {
+    Map<String, Configuration> map = new HashMap<>();
+    for (ComponentDto project : projects) {
+      map.put(project.uuid(), config);
+    }
+    return map;
+  }
+}
index 1fcc474f594a1c55f4636c5dbd176f8efe6a27fc..21c7cb873906b22f2f7d32b30cfd226edb76fde8 100644 (file)
@@ -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<String, String> 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());
index 86dc92e961a3a272a740f3eecb4fde9803186879..7415178a93d067e845c34a66afccfa42b76eace4 100644 (file)
@@ -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<String, String> scannerProperties = ImmutableMap.of(
       "sonar.analysis.revision", "ab45d24",
index a8d82fc9770e815dd458220f915b92f9adaca1a0..1f0bb7d2e7f5de2641465559f84a15d0870a9d19 100644 (file)
@@ -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<QGChangeEventListener.ChangedIssue> CHANGED_ISSUES_ARE_IGNORED = emptySet();
 
index a49233c995a70ce878a6e399fbc682041129b73c..14d91c6578275610cb57cd0061ac1afae169948c 100644 (file)
@@ -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);
index e54e9d3f8d8af9531689a3d38fbffee66193c461..dbc6db0af65c0ed9174432a91d63a5d507e3f36a 100644 (file)
@@ -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);
index 2d810712ff53a30a8bd27817cb3d6352e46eb2be..702462c9f8e939f77f15c0da2fd0e52e3f8f3add 100644 (file)
@@ -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;
index 31a46f376601d6262cba451fe06fac5ea05bc63c..d9d2e512908b0b5e9026e493ce0ef6ed8034e01f 100644 (file)
@@ -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 <E extends Enum<E>> Collector<E, ?, EnumSet<E>> toEnumSet(Class<E> enumClass) {
+    return Collectors.toCollection(() -> EnumSet.noneOf(enumClass));
+  }
+
   /**
    * Delegates to {@link java.util.stream.Collectors#toCollection(Supplier)}.
    */
index f6a72667d62be0285d5d8d1906a8708156467ea6..89c5ededa5704ecd1280092946e33ce0a2df9dbc 100644 (file)
@@ -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
index 398d3f3756a665cd6abf68d10218adcb8e08d917..68c8ae6cec6fbc27d02e7ebd421a6deb1c703db8 100644 (file)
@@ -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<Integer> res = Arrays.asList(1, 2, 3, 4, 5).stream().collect(toSet(30));
+    Set<Integer> 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<MyEnum> 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<MyEnum> res = Stream.<MyEnum>empty().collect(MoreCollectors.toEnumSet(MyEnum.class));
+    assertThat(res).isInstanceOf(EnumSet.class)
+      .isEmpty();
+  }
+
   @Test
   public void toArrayList_builds_an_ArrayList() {
     List<Integer> 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
+  }
 }
index d0eccf976e846a67a674b132194d1ba86df81d9a..2c1545efbd13a0c4f8f95d0545f37aeb4f288ec4 100644 (file)
@@ -2413,8 +2413,26 @@ public final class CoreMetrics {
   /**
    * @since 4.5
    */
-  public static final Metric<String> DEVELOPMENT_COST = new Metric.Builder(DEVELOPMENT_COST_KEY, "SQALE Development Cost", Metric.ValueType.STRING)
-    .setDescription("SQALE development cost")
+  public static final Metric<String> 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<String> 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)
index 96d02ed89cf5d4d36ee644e8b8ed539f95208e12..8605fd9798c0e96fdbbd5966f7a73729e9a178a1 100644 (file)
  */
 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<String> 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 (file)
index 0000000..dbe1d71
--- /dev/null
@@ -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 (file)
index 0000000..89b24ec
--- /dev/null
@@ -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 (file)
index 0000000..a92d664
--- /dev/null
@@ -0,0 +1,3 @@
+line 1
+line 2
+line 3
index 125d4475cb87327ae7ad6b9e25f5673f616af93b..57c0aca0d3e16313386ad59946541babe53796db 100644 (file)
@@ -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<String, Double> 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);
   }
index b8f96966eb195f2e367cab193223786777aa3376..0f2d03d7aac0e3d6e8e0b86e9f51ac00a44b7888 100644 (file)
@@ -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 (file)
index 0000000..a47587a
--- /dev/null
@@ -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.Measure> 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<String, String, String> valuesByComponentAndMetric = HashBasedTable.create();
+
+    void assertEquals(MeasuresDump dump) {
+      assertThat(valuesByComponentAndMetric.size()).isEqualTo(dump.valuesByComponentAndMetric.size());
+      for (Table.Cell<String, String, String> cell : valuesByComponentAndMetric.cellSet()) {
+        assertThat(dump.valuesByComponentAndMetric.get(cell.getRowKey(), cell.getColumnKey()))
+          .as("Measure " + cell.getColumnKey() + " on component " + cell.getRowKey())
+          .isEqualTo(cell.getValue());
+      }
+    }
+  }
+}
index 3c033f0acc6e98624d789167f03738921ecc0561..2997929905c50b5350943ef95f51f7426146965d 100644 (file)
@@ -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
index fcdcc89a98d63b3c48ecddfab6a842e477815b07..92c6bed436b63e0901588e74cdf0abda02f75ae1 100644 (file)
@@ -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<String, Object> payload = jsonToMap(request.getJson());
+    assertThat(payload.get("status")).isEqualTo("SUCCESS");
+    assertThat(payload.get("analysedAt")).isNotNull();
+    Map<String, String> project = (Map<String, String>) 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<String, Object> gate = (Map<String, Object>) 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 (file)
index 0000000..162413c
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0"?><!-- Generated by Sonar -->
+<profile>
+  <name>one-bug-per-line-profile</name>
+  <language>xoo</language>
+  <rules>
+    <rule>
+      <repositoryKey>xoo</repositoryKey>
+      <key>OneBugIssuePerLine</key>
+      <priority>MAJOR</priority>
+    </rule>
+  </rules>
+</profile>