]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20429 Raise event in case plugin has been updated
authorJacek <jacek.poreda@sonarsource.com>
Wed, 20 Sep 2023 06:55:43 +0000 (08:55 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 21 Sep 2023 20:02:44 +0000 (20:02 +0000)
server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistEventsStepIT.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/event/Event.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/qualityprofile/RegisterQualityProfileStatusStep.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/IssueDetectionEventsStep.java [new file with mode: 0644]
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PersistEventsStep.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/IssueDetectionEventsStepTest.java [new file with mode: 0644]
server/sonar-db-dao/src/main/java/org/sonar/db/event/EventDto.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/projectanalysis/ws/EventCategory.java
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 2cafcaca9a1bceb8d1022b88aef724adc88f48ab..348b0bf02a6e8335b90e1fc5cc51e7a25194b3bd 100644 (file)
@@ -19,7 +19,6 @@
  */
 package org.sonar.ce.task.projectanalysis.step;
 
-import com.google.common.collect.ImmutableList;
 import java.util.Collections;
 import java.util.Date;
 import java.util.List;
@@ -42,6 +41,7 @@ import org.sonar.db.component.ComponentDto;
 import org.sonar.db.event.EventDto;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 import static org.sonar.ce.task.projectanalysis.component.Component.Type.DIRECTORY;
@@ -49,6 +49,7 @@ import static org.sonar.ce.task.projectanalysis.component.Component.Type.FILE;
 import static org.sonar.ce.task.projectanalysis.component.Component.Type.PROJECT;
 import static org.sonar.ce.task.projectanalysis.component.ReportComponent.builder;
 import static org.sonar.db.event.EventDto.CATEGORY_ALERT;
+import static org.sonar.db.event.EventDto.CATEGORY_ISSUE_DETECTION;
 import static org.sonar.db.event.EventDto.CATEGORY_PROFILE;
 import static org.sonar.db.event.EventDto.CATEGORY_VERSION;
 
@@ -82,10 +83,10 @@ public class PersistEventsStepIT extends BaseStepTest {
   @Rule
   public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule();
 
-  private Date someDate = new Date(150000000L);
+  private final Date someDate = new Date(150000000L);
 
-  private EventRepository eventRepository = mock(EventRepository.class);
-  private UuidFactory uuidFactory = UuidFactoryImpl.INSTANCE;
+  private final EventRepository eventRepository = mock(EventRepository.class);
+  private final UuidFactory uuidFactory = UuidFactoryImpl.INSTANCE;
 
   private PersistEventsStep underTest;
 
@@ -142,7 +143,7 @@ public class PersistEventsStepIT extends BaseStepTest {
     when(system2.now()).thenReturn(NOW);
     treeRootHolder.setRoot(ROOT);
     Event alert = Event.createAlert("Failed", null, "Open issues > 0");
-    when(eventRepository.getEvents()).thenReturn(ImmutableList.of(alert));
+    when(eventRepository.getEvents()).thenReturn(List.of(alert));
 
     underTest.execute(new TestComputationStepContext());
 
@@ -166,7 +167,7 @@ public class PersistEventsStepIT extends BaseStepTest {
     when(system2.now()).thenReturn(NOW);
     treeRootHolder.setRoot(ROOT);
     Event profile = Event.createProfile("foo", null, "bar");
-    when(eventRepository.getEvents()).thenReturn(ImmutableList.of(profile));
+    when(eventRepository.getEvents()).thenReturn(List.of(profile));
 
     underTest.execute(new TestComputationStepContext());
 
@@ -185,6 +186,33 @@ public class PersistEventsStepIT extends BaseStepTest {
     assertThat(eventDto.getCreatedAt()).isEqualTo(NOW);
   }
 
+  @Test
+  public void execute_whenIssueDetectionEventRaised_shouldPersist() {
+    when(system2.now()).thenReturn(NOW);
+    treeRootHolder.setRoot(ROOT);
+    Event issueDetection = Event.createIssueDetection("Capabilities have changed (Xoo)");
+    when(eventRepository.getEvents()).thenReturn(List.of(issueDetection));
+
+    underTest.execute(new TestComputationStepContext());
+
+    assertThat(dbTester.countRowsOfTable(dbTester.getSession(), "events")).isEqualTo(2);
+    List<EventDto> eventDtos = dbTester.getDbClient().eventDao().selectByComponentUuid(dbTester.getSession(), ROOT.getUuid());
+    assertThat(eventDtos)
+      .extracting(EventDto::getCategory)
+      .containsOnly(CATEGORY_ISSUE_DETECTION, CATEGORY_VERSION);
+    EventDto eventDto = eventDtos.stream()
+      .filter(t -> CATEGORY_ISSUE_DETECTION.equals(t.getCategory()))
+      .findAny()
+      .orElseGet(() -> fail("Issue detection event not found"));
+    assertThat(eventDto.getComponentUuid()).isEqualTo(ROOT.getUuid());
+    assertThat(eventDto.getName()).isEqualTo(issueDetection.getName());
+    assertThat(eventDto.getDescription()).isEqualTo(issueDetection.getDescription());
+    assertThat(eventDto.getCategory()).isEqualTo(CATEGORY_ISSUE_DETECTION);
+    assertThat(eventDto.getData()).isNull();
+    assertThat(eventDto.getDate()).isEqualTo(analysisMetadataHolder.getAnalysisDate());
+    assertThat(eventDto.getCreatedAt()).isEqualTo(NOW);
+  }
+
   @Test
   public void keep_one_event_by_version() {
     ComponentDto projectDto = dbTester.components().insertPublicProject().getMainBranchComponent();
index d2add87213071466030950884bfaf89fa340f634..1b2ccff2f89c67a8c297f729834158303df75467 100644 (file)
@@ -41,6 +41,13 @@ public class Event {
     this.description = description;
   }
 
+  private Event(String name, Category category) {
+    this.name = requireNonNull(name);
+    this.category = requireNonNull(category);
+    this.data = null;
+    this.description = null;
+  }
+
   public static Event createAlert(String name, @Nullable String data, @Nullable String description) {
     return new Event(name, Category.ALERT, data, description);
   }
@@ -49,6 +56,10 @@ public class Event {
     return new Event(name, Category.PROFILE, data, description);
   }
 
+  public static Event createIssueDetection(String name) {
+    return new Event(name, Category.ISSUE_DETECTION);
+  }
+
   public String getName() {
     return name;
   }
@@ -85,7 +96,7 @@ public class Event {
   }
 
   public enum Category {
-    ALERT, PROFILE
+    ALERT, PROFILE, ISSUE_DETECTION
   }
 
 }
index eb35f75a562f040dfd3cd8e0c6fe7b8e6d42cdff..b691fafeb63905ecd955820dba8b2b5583fe897f 100644 (file)
@@ -95,7 +95,7 @@ public class RegisterQualityProfileStatusStep implements ComputationStep {
       QualityProfile baseProfile = baseProfiles.get(profile.getQpKey());
       if (baseProfile == null) {
         register(profile, ADDED);
-      } else if  (profile.getRulesUpdatedAt().after(baseProfile.getRulesUpdatedAt())) {
+      } else if (profile.getRulesUpdatedAt().after(baseProfile.getRulesUpdatedAt())) {
         register(baseProfile, UPDATED);
       } else {
         register(baseProfile, UNCHANGED);
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/IssueDetectionEventsStep.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/IssueDetectionEventsStep.java
new file mode 100644 (file)
index 0000000..3e26f62
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.ce.task.projectanalysis.step;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Optional;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.resources.Language;
+import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
+import org.sonar.ce.task.projectanalysis.analysis.ScannerPlugin;
+import org.sonar.ce.task.projectanalysis.component.Component;
+import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
+import org.sonar.ce.task.projectanalysis.event.Event;
+import org.sonar.ce.task.projectanalysis.event.EventRepository;
+import org.sonar.ce.task.projectanalysis.language.LanguageRepository;
+import org.sonar.ce.task.projectanalysis.measure.Measure;
+import org.sonar.ce.task.projectanalysis.measure.MeasureRepository;
+import org.sonar.ce.task.projectanalysis.metric.Metric;
+import org.sonar.ce.task.projectanalysis.metric.MetricRepository;
+import org.sonar.ce.task.step.ComputationStep;
+import org.sonar.server.qualityprofile.QPMeasureData;
+import org.sonar.server.qualityprofile.QualityProfile;
+
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Computation of Issue Detection events
+ * As it depends upon {@link CoreMetrics#QUALITY_PROFILES_KEY}, it must be executed after {@link ComputeQProfileMeasureStep}
+ */
+public class IssueDetectionEventsStep implements ComputationStep {
+  private final TreeRootHolder treeRootHolder;
+  private final MetricRepository metricRepository;
+  private final MeasureRepository measureRepository;
+  private final EventRepository eventRepository;
+  private final LanguageRepository languageRepository;
+  private final AnalysisMetadataHolder analysisMetadataHolder;
+
+  public IssueDetectionEventsStep(TreeRootHolder treeRootHolder, MetricRepository metricRepository,
+    MeasureRepository measureRepository, LanguageRepository languageRepository, EventRepository eventRepository,
+    AnalysisMetadataHolder analysisMetadataHolder) {
+    this.treeRootHolder = treeRootHolder;
+    this.metricRepository = metricRepository;
+    this.measureRepository = measureRepository;
+    this.eventRepository = eventRepository;
+    this.languageRepository = languageRepository;
+    this.analysisMetadataHolder = analysisMetadataHolder;
+  }
+
+  @Override
+  public void execute(Context context) {
+    executeForBranch(treeRootHolder.getRoot());
+  }
+
+  private void executeForBranch(Component branchComponent) {
+    Metric qualityProfileMetric = metricRepository.getByKey(CoreMetrics.QUALITY_PROFILES_KEY);
+    Optional<Measure> baseMeasure = measureRepository.getBaseMeasure(branchComponent, qualityProfileMetric);
+    if (baseMeasure.isEmpty()) {
+      // first analysis -> do not generate events
+      return;
+    }
+
+    // Load profiles used in current analysis for which at least one file of the corresponding language exists
+    Optional<Measure> rawMeasure = measureRepository.getRawMeasure(branchComponent, qualityProfileMetric);
+    if (rawMeasure.isEmpty()) {
+      // No qualify profile computed on the project
+      return;
+    }
+    Map<String, QualityProfile> rawProfiles = QPMeasureData.fromJson(rawMeasure.get().getStringValue()).getProfilesByKey();
+    detectIssueCapabilitiesChanged(rawProfiles.values());
+  }
+
+  private void detectIssueCapabilitiesChanged(Collection<QualityProfile> rawProfiles) {
+    Map<String, ScannerPlugin> scannerPluginsByKey = analysisMetadataHolder.getScannerPluginsByKey();
+    long lastAnalysisDate = requireNonNull(analysisMetadataHolder.getBaseAnalysis()).getCreatedAt();
+    for (QualityProfile profile : rawProfiles) {
+      String languageKey = profile.getLanguageKey();
+      ScannerPlugin scannerPlugin = scannerPluginsByKey.get(languageKey);
+      if (scannerPlugin == null) {
+        // nothing to do as the language is not supported
+        continue;
+      }
+
+      if (scannerPlugin.getUpdatedAt() > lastAnalysisDate) {
+        String languageName = languageRepository.find(languageKey)
+          .map(Language::getName)
+          .orElse(languageKey);
+        Event event = Event.createIssueDetection(format("Capabilities have been updated (%s)", languageName));
+        eventRepository.add(event);
+      }
+    }
+  }
+
+  @Override
+  public String getDescription() {
+    return "Generate Issue Detection events";
+  }
+}
index 5535efdbc1a06e18cda899bae0e1253e63bb211e..711427f678db61bafd74989f64fa8089c1f3e010 100644 (file)
@@ -119,14 +119,11 @@ public class PersistEventsStep implements ComputationStep {
     }
 
     private String convertCategory(Event.Category category) {
-      switch (category) {
-        case ALERT:
-          return EventDto.CATEGORY_ALERT;
-        case PROFILE:
-          return EventDto.CATEGORY_PROFILE;
-        default:
-          throw new IllegalArgumentException(String.format("Unsupported category %s", category.name()));
-      }
+      return switch (category) {
+        case ALERT -> EventDto.CATEGORY_ALERT;
+        case PROFILE -> EventDto.CATEGORY_PROFILE;
+        case ISSUE_DETECTION -> EventDto.CATEGORY_ISSUE_DETECTION;
+      };
     }
 
   }
index e93279055b4dc38b6e2940de6464bde9c86f94b8..6432f2a3aef185afcb4b162fb893bef86b31bbe6 100644 (file)
@@ -86,6 +86,7 @@ public class ReportComputationSteps extends AbstractComputationSteps {
 
     // Must be executed after computation of quality gate measure
     QualityGateEventsStep.class,
+    IssueDetectionEventsStep.class,
 
     HandleUnanalyzedLanguagesStep.class,
 
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/IssueDetectionEventsStepTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/IssueDetectionEventsStepTest.java
new file mode 100644 (file)
index 0000000..491da8e
--- /dev/null
@@ -0,0 +1,236 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.ce.task.projectanalysis.step;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.resources.AbstractLanguage;
+import org.sonar.api.resources.Language;
+import org.sonar.ce.task.projectanalysis.analysis.Analysis;
+import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
+import org.sonar.ce.task.projectanalysis.analysis.ScannerPlugin;
+import org.sonar.ce.task.projectanalysis.component.Component;
+import org.sonar.ce.task.projectanalysis.component.ReportComponent;
+import org.sonar.ce.task.projectanalysis.component.TreeRootHolderRule;
+import org.sonar.ce.task.projectanalysis.event.Event;
+import org.sonar.ce.task.projectanalysis.event.EventRepository;
+import org.sonar.ce.task.projectanalysis.language.LanguageRepository;
+import org.sonar.ce.task.projectanalysis.measure.Measure;
+import org.sonar.ce.task.projectanalysis.measure.MeasureRepository;
+import org.sonar.ce.task.projectanalysis.metric.Metric;
+import org.sonar.ce.task.projectanalysis.metric.MetricRepository;
+import org.sonar.ce.task.step.TestComputationStepContext;
+import org.sonar.server.qualityprofile.QPMeasureData;
+import org.sonar.server.qualityprofile.QualityProfile;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.groups.Tuple.tuple;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+public class IssueDetectionEventsStepTest {
+  @Rule
+  public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule();
+
+  private static final String QP_NAME_1 = "qp_1";
+  private static final String QP_NAME_2 = "qp_2";
+  private static final String LANGUAGE_KEY_1 = "language_key1";
+  private static final String LANGUAGE_KEY_2 = "language_key2";
+
+  private final MetricRepository metricRepository = mock(MetricRepository.class);
+  private final MeasureRepository measureRepository = mock(MeasureRepository.class);
+  private final LanguageRepository languageRepository = mock(LanguageRepository.class);
+  private final EventRepository eventRepository = mock(EventRepository.class);
+  private final ArgumentCaptor<Event> eventArgumentCaptor = ArgumentCaptor.forClass(Event.class);
+  private final AnalysisMetadataHolder analysisMetadataHolder = mock(AnalysisMetadataHolder.class);
+
+  private final Metric qualityProfileMetric = mock(Metric.class);
+
+  private final IssueDetectionEventsStep underTest = new IssueDetectionEventsStep(treeRootHolder, metricRepository, measureRepository, languageRepository, eventRepository,
+    analysisMetadataHolder);
+
+  @Before
+  public void setUp() {
+    when(metricRepository.getByKey(CoreMetrics.QUALITY_PROFILES_KEY)).thenReturn(qualityProfileMetric);
+    treeRootHolder.setRoot(ReportComponent.builder(Component.Type.PROJECT, 1).setUuid("uuid").setKey("key").build());
+  }
+
+  @Test
+  public void execute_whenNoBaseMeasure_shouldNotRaiseEvent() {
+    when(measureRepository.getBaseMeasure(treeRootHolder.getRoot(), qualityProfileMetric)).thenReturn(Optional.empty());
+
+    underTest.execute(new TestComputationStepContext());
+
+    verifyNoMoreInteractions(eventRepository);
+  }
+
+  @Test
+  public void execute_whenNoRawMeasure_shouldNotRaiseEvent() {
+    when(measureRepository.getBaseMeasure(treeRootHolder.getRoot(), qualityProfileMetric)).thenReturn(Optional.of(newMeasure()));
+    when(measureRepository.getRawMeasure(treeRootHolder.getRoot(), qualityProfileMetric)).thenReturn(Optional.empty());
+
+    underTest.execute(new TestComputationStepContext());
+
+    verifyNoMoreInteractions(eventRepository);
+  }
+
+  @Test
+  public void execute_whenNoBaseMeasureAndQPMeasureIsEmpty_shouldNotRaiseEvent() {
+    when(measureRepository.getBaseMeasure(treeRootHolder.getRoot(), qualityProfileMetric)).thenReturn(Optional.empty());
+    when(measureRepository.getRawMeasure(treeRootHolder.getRoot(), qualityProfileMetric)).thenReturn(Optional.of(newMeasure()));
+
+    underTest.execute(new TestComputationStepContext());
+
+    verifyNoMoreInteractions(eventRepository);
+  }
+
+  @Test
+  public void execute_whenAnalyzerChangedAndLanguageNotSupported_shouldSkipRaisingEvent() {
+    QualityProfile qp1 = qp(QP_NAME_1, LANGUAGE_KEY_1, new Date());
+
+    mockLanguageInRepository(LANGUAGE_KEY_1);
+    when(measureRepository.getBaseMeasure(treeRootHolder.getRoot(), qualityProfileMetric)).thenReturn(Optional.of(newMeasure()));
+    when(measureRepository.getRawMeasure(treeRootHolder.getRoot(), qualityProfileMetric)).thenReturn(Optional.of(newMeasure(qp1)));
+
+    when(analysisMetadataHolder.getScannerPluginsByKey()).thenReturn(Collections.emptyMap());
+    when(analysisMetadataHolder.getBaseAnalysis()).thenReturn(new Analysis.Builder().setUuid("uuid").setCreatedAt(1L).build());
+
+    underTest.execute(new TestComputationStepContext());
+
+    verifyNoMoreInteractions(eventRepository);
+  }
+
+  @Test
+  public void execute_whenAnalyzerChanged_shouldRaiseEventForAllLanguages() {
+    QualityProfile qp1 = qp(QP_NAME_1, LANGUAGE_KEY_1, new Date());
+    QualityProfile qp2 = qp(QP_NAME_2, LANGUAGE_KEY_2, new Date());
+
+    mockLanguageInRepository(LANGUAGE_KEY_1);
+    mockLanguageInRepository(LANGUAGE_KEY_2);
+
+    when(measureRepository.getBaseMeasure(treeRootHolder.getRoot(), qualityProfileMetric)).thenReturn(Optional.of(newMeasure()));
+    when(measureRepository.getRawMeasure(treeRootHolder.getRoot(), qualityProfileMetric)).thenReturn(Optional.of(newMeasure(qp1, qp2)));
+
+    ScannerPlugin scannerPluginLanguage1 = mockScannerPlugin(LANGUAGE_KEY_1, 3L);
+    ScannerPlugin scannerPluginLanguage2 = mockScannerPlugin(LANGUAGE_KEY_2, 2L);
+
+    when(analysisMetadataHolder.getScannerPluginsByKey()).thenReturn(Map.of(LANGUAGE_KEY_1, scannerPluginLanguage1, LANGUAGE_KEY_2, scannerPluginLanguage2));
+    when(analysisMetadataHolder.getBaseAnalysis()).thenReturn(new Analysis.Builder().setUuid("uuid").setCreatedAt(1L).build());
+
+    underTest.execute(new TestComputationStepContext());
+
+    verify(eventRepository, times(2)).add(eventArgumentCaptor.capture());
+    verifyNoMoreInteractions(eventRepository);
+
+    assertThat(eventArgumentCaptor.getAllValues())
+      .extracting(Event::getCategory, Event::getName, Event::getDescription)
+      .containsExactlyInAnyOrder(tuple(Event.Category.ISSUE_DETECTION, "Capabilities have been updated (language_key1_name)", null),
+        tuple(Event.Category.ISSUE_DETECTION, "Capabilities have been updated (language_key2_name)", null));
+  }
+
+  @Test
+  public void execute_whenAnalyzerChangedAndAnalyzerUpdateDateBeforeAnalysis_shouldNotRaiseEvent() {
+    QualityProfile qp1 = qp(QP_NAME_1, LANGUAGE_KEY_1, new Date());
+
+    mockLanguageInRepository(LANGUAGE_KEY_1);
+
+    when(measureRepository.getBaseMeasure(treeRootHolder.getRoot(), qualityProfileMetric)).thenReturn(Optional.of(newMeasure()));
+    when(measureRepository.getRawMeasure(treeRootHolder.getRoot(), qualityProfileMetric)).thenReturn(Optional.of(newMeasure(qp1)));
+
+    ScannerPlugin scannerPluginLanguage1 = mockScannerPlugin(LANGUAGE_KEY_1, 1L);
+
+    when(analysisMetadataHolder.getScannerPluginsByKey()).thenReturn(Map.of(LANGUAGE_KEY_1, scannerPluginLanguage1));
+    when(analysisMetadataHolder.getBaseAnalysis()).thenReturn(new Analysis.Builder().setUuid("uuid").setCreatedAt(3L).build());
+
+    underTest.execute(new TestComputationStepContext());
+
+    verifyNoMoreInteractions(eventRepository);
+  }
+
+  @Test
+  public void execute_whenAnalyzerDidNotChange_shouldNotRaiseEvent() {
+    QualityProfile qp = qp(QP_NAME_1, LANGUAGE_KEY_1, new Date());
+
+    mockLanguage1AsNotInRepository();
+
+    when(analysisMetadataHolder.getBaseAnalysis()).thenReturn(new Analysis.Builder().setUuid("uuid").setCreatedAt(1L).build());
+
+    when(measureRepository.getBaseMeasure(treeRootHolder.getRoot(), qualityProfileMetric)).thenReturn(Optional.of(newMeasure()));
+    when(measureRepository.getRawMeasure(treeRootHolder.getRoot(), qualityProfileMetric)).thenReturn(Optional.of(newMeasure(qp)));
+
+    underTest.execute(new TestComputationStepContext());
+
+    verifyNoMoreInteractions(eventRepository);
+  }
+
+  private void mockLanguageInRepository(String languageKey) {
+    Language language = new AbstractLanguage(languageKey, languageKey + "_name") {
+      @Override
+      public String[] getFileSuffixes() {
+        return new String[0];
+      }
+    };
+    when(languageRepository.find(languageKey)).thenReturn(Optional.of(language));
+  }
+
+  private void mockLanguage1AsNotInRepository() {
+    when(languageRepository.find(LANGUAGE_KEY_1)).thenReturn(Optional.empty());
+  }
+
+  private static QualityProfile qp(String qpName, String languageKey, Date date) {
+    return new QualityProfile(qpName + "-" + languageKey, qpName, languageKey, date);
+  }
+
+  /**
+   * Just a trick to use variable args which is shorter than writing new QualityProfile[] { }
+   */
+  private static QualityProfile[] arrayOf(QualityProfile... qps) {
+    return qps;
+  }
+
+  private static Measure newMeasure(@Nullable QualityProfile... qps) {
+    return Measure.newMeasureBuilder().create(toJson(qps));
+  }
+
+  private static String toJson(@Nullable QualityProfile... qps) {
+    List<QualityProfile> qualityProfiles = qps != null ? Arrays.asList(qps) : Collections.emptyList();
+    return QPMeasureData.toJson(new QPMeasureData(qualityProfiles));
+  }
+
+  private static ScannerPlugin mockScannerPlugin(String repositoryKey, long updatedAt) {
+    ScannerPlugin scannerPluginLanguage = mock(ScannerPlugin.class);
+    when(scannerPluginLanguage.getUpdatedAt()).thenReturn(updatedAt);
+    when(scannerPluginLanguage.getKey()).thenReturn(repositoryKey);
+    return scannerPluginLanguage;
+  }
+}
index 451804fee04ce00620a2689f8ec82da295cd2232..c03d11d7cf62320d9447f23b4da3238d53b60b2c 100644 (file)
@@ -32,7 +32,7 @@ public class EventDto {
   public static final String CATEGORY_ALERT = "Alert";
   public static final String CATEGORY_PROFILE = "Profile";
   public static final String CATEGORY_DEFINITION_CHANGE = "Definition change";
-
+  public static final String CATEGORY_ISSUE_DETECTION = "Issue Detection";
   private String uuid;
   private String analysisUuid;
   private String componentUuid;
index f8f55e3c29cd15a23558a5bf8fd1707539c0291a..1e3371820e116031705e9a2c809097f4e70bbb7d 100644 (file)
 package org.sonar.server.projectanalysis.ws;
 
 public enum EventCategory {
-  VERSION("Version"), OTHER("Other"), QUALITY_PROFILE("Profile"), QUALITY_GATE("Alert"), DEFINITION_CHANGE("Definition change");
+  VERSION("Version"),
+  OTHER("Other"),
+  QUALITY_PROFILE("Profile"),
+  QUALITY_GATE("Alert"),
+  DEFINITION_CHANGE("Definition change"),
+  ISSUE_DETECTION("Issue Detection");
 
   private final String label;
 
index 9d48dd21d658dce0751b2d3e6afa8e893bd94760..2291d1fea4f1a86d1dc0e55a4192dbe560555978 100644 (file)
@@ -516,6 +516,7 @@ event.category.VERSION=Version
 event.category.QUALITY_GATE=Quality Gate
 event.category.QUALITY_PROFILE=Quality Profile
 event.category.DEFINITION_CHANGE=Definition Change
+event.category.ISSUE_DETECTION=Issue Detection
 event.category.OTHER=Other
 event.quality_gate.still_x=Still {status}
 event.quality_gate.ERROR=Failed