]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6569 add step to compute Quality Gate events in CE
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Mon, 1 Jun 2015 10:05:47 +0000 (12:05 +0200)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Thu, 4 Jun 2015 12:36:03 +0000 (14:36 +0200)
server/sonar-server/src/main/java/org/sonar/server/computation/step/ComputationSteps.java
server/sonar-server/src/main/java/org/sonar/server/computation/step/QualityGateEventsStep.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/step/QualityGateEventsStepTest.java [new file with mode: 0644]

index 25190675f4cb37887a1220b8119e9bf1571f1e70..42b74c6c27d86f7e4006c5844aa8b1dbf5b3ad7a 100644 (file)
@@ -41,7 +41,6 @@ public class ComputationSteps {
 
       // Builds Component tree
       BuildComponentTreeStep.class,
-
       PopulateComponentsUuidAndKeyStep.class,
       ValidateProjectStep.class,
 
@@ -50,6 +49,7 @@ public class ComputationSteps {
 
       // data computation
       QualityProfileEventsStep.class,
+      QualityGateEventsStep.class,
 
       // Persist data
       PersistComponentsAndSnapshotsStep.class,
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/step/QualityGateEventsStep.java b/server/sonar-server/src/main/java/org/sonar/server/computation/step/QualityGateEventsStep.java
new file mode 100644 (file)
index 0000000..2335b32
--- /dev/null
@@ -0,0 +1,153 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.step;
+
+import com.google.common.base.Optional;
+import javax.annotation.Nullable;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.batch.protocol.output.BatchReport;
+import org.sonar.core.measure.db.MeasureDto;
+import org.sonar.server.computation.component.Component;
+import org.sonar.server.computation.component.DepthTraversalTypeAwareVisitor;
+import org.sonar.server.computation.component.TreeRootHolder;
+import org.sonar.server.computation.event.Event;
+import org.sonar.server.computation.event.EventRepository;
+import org.sonar.server.computation.measure.MeasureRepository;
+
+public class QualityGateEventsStep implements ComputationStep {
+  public static final Logger LOGGER = Loggers.get(QualityGateEventsStep.class);
+  private final TreeRootHolder treeRootHolder;
+  private final EventRepository eventRepository;
+  private final MeasureRepository measureRepository;
+
+  public QualityGateEventsStep(TreeRootHolder treeRootHolder, EventRepository eventRepository, MeasureRepository measureRepository) {
+    this.eventRepository = eventRepository;
+    this.measureRepository = measureRepository;
+    this.treeRootHolder = treeRootHolder;
+  }
+
+  @Override
+  public void execute() {
+    new DepthTraversalTypeAwareVisitor(Component.Type.PROJECT, DepthTraversalTypeAwareVisitor.Order.PRE_ORDER) {
+      @Override
+      public void visitProject(Component project) {
+        executeForProject(project);
+      }
+    }.visit(treeRootHolder.getRoot());
+  }
+
+  private void executeForProject(Component project) {
+    Optional<BatchReport.Measure> currentStatus = measureRepository.findCurrent(project, CoreMetrics.ALERT_STATUS);
+    if (!currentStatus.isPresent()) {
+      return;
+    }
+    Optional<AlertStatus> alertLevel = parse(currentStatus.get().getAlertStatus());
+    if (!alertLevel.isPresent()) {
+      return;
+    }
+
+    checkQualityGateStatusChange(project, alertLevel.get(), currentStatus.get().getAlertText());
+  }
+
+  private void checkQualityGateStatusChange(Component project, AlertStatus currentStatus, String alertText) {
+    Optional<MeasureDto> previousMeasure = measureRepository.findPrevious(project, CoreMetrics.ALERT_STATUS);
+    if (!previousMeasure.isPresent()) {
+      checkNewQualityGate(project, currentStatus, alertText);
+      return;
+    }
+
+    Optional<AlertStatus> previousQGStatus = parse(previousMeasure.get().getAlertStatus());
+    if (!previousQGStatus.isPresent()) {
+      LOGGER.warn("Previous alterStatus for project %s is not a supported value. Can not compute Quality Gate event");
+      checkNewQualityGate(project, currentStatus, alertText);
+      return;
+    }
+
+    if (previousQGStatus.get() != currentStatus) {
+      // The alert status has changed
+      String alertName = String.format("%s (was %s)", currentStatus.getColorName(), previousQGStatus.get().getColorName());
+      createEvent(project, alertName, alertText);
+      // FIXME @Simon uncomment and/or rewrite code below when implementing notifications in CE
+      // There was already a Orange/Red alert, so this is no new alert: it has just changed
+      // boolean isNewAlert = previousQGStatus == AlertStatus.OK;
+      // notifyUsers(project, alertName, alertText, alertLevel, isNewAlert);
+    }
+  }
+
+  private void checkNewQualityGate(Component project, AlertStatus currentStatus, String alertText) {
+    if (currentStatus != AlertStatus.OK) {
+      // There were no defined alerts before, so this one is a new one
+      createEvent(project, currentStatus.getColorName(), alertText);
+      // notifyUsers(project, alertName, alertText, alertLevel, true);
+    }
+  }
+
+  private static Optional<AlertStatus> parse(@Nullable String alertStatus) {
+    if (alertStatus == null) {
+      return Optional.absent();
+    }
+
+    try {
+      return Optional.of(AlertStatus.valueOf(alertStatus));
+    } catch (IllegalArgumentException e) {
+      LOGGER.error(String.format("Unsupported alertStatus value '%s' can not be parsed to AlertStatus", alertStatus));
+      return Optional.absent();
+    }
+  }
+
+  private enum AlertStatus {
+    OK("Green"), WARN("Orange"), ERROR("Red");
+
+    private String colorName;
+
+    AlertStatus(String colorName) {
+      this.colorName = colorName;
+    }
+
+    public String getColorName() {
+      return colorName;
+    }
+  }
+
+  // FIXME @Simon uncomment and/or rewrite code below when implementing notifications in CE
+  // protected void notifyUsers(Component project, String alertName, String alertText, AlertStatus alertLevel, boolean isNewAlert) {
+  // Notification notification = new Notification("alerts")
+  // .setDefaultMessage("Alert on " + project.getName() + ": " + alertName)
+  // .setFieldValue("projectName", project.getName())
+  // .setFieldValue("projectKey", project.getKey())
+  // .setFieldValue("projectId", String.valueOf(project.getId()))
+  // .setFieldValue("alertName", alertName)
+  // .setFieldValue("alertText", alertText)
+  // .setFieldValue("alertLevel", alertLevel.toString())
+  // .setFieldValue("isNewAlert", Boolean.toString(isNewAlert));
+  // notificationManager.scheduleForSending(notification);
+  // }
+
+  private void createEvent(Component project, String name, String description) {
+    eventRepository.add(project, Event.createAlert(name, null, description));
+  }
+
+  @Override
+  public String getDescription() {
+    return "Generate Quality Gate Events";
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/step/QualityGateEventsStepTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/step/QualityGateEventsStepTest.java
new file mode 100644 (file)
index 0000000..f050d68
--- /dev/null
@@ -0,0 +1,208 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.step;
+
+import com.google.common.base.Optional;
+import javax.annotation.Nullable;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.sonar.batch.protocol.output.BatchReport;
+import org.sonar.core.measure.db.MeasureDto;
+import org.sonar.server.computation.batch.TreeRootHolderRule;
+import org.sonar.server.computation.component.Component;
+import org.sonar.server.computation.component.DumbComponent;
+import org.sonar.server.computation.event.Event;
+import org.sonar.server.computation.event.EventRepository;
+import org.sonar.server.computation.measure.MeasureRepository;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS;
+
+public class QualityGateEventsStepTest {
+  private static final DumbComponent PROJECT_COMPONENT = new DumbComponent(1, Component.Type.PROJECT, new DumbComponent(2, Component.Type.MODULE));
+  private static final String INVALID_ALERT_STATUS = "trololo";
+
+  @Rule
+  public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule();
+
+  private ArgumentCaptor<Event> eventArgumentCaptor = ArgumentCaptor.forClass(Event.class);
+
+  private EventRepository eventRepository = mock(EventRepository.class);
+  private MeasureRepository measureRepository = mock(MeasureRepository.class);
+  private QualityGateEventsStep underTest = new QualityGateEventsStep(treeRootHolder, eventRepository, measureRepository);
+  public static final String ALERT_TEXT = "alert text";
+
+  @Before
+  public void setUp() throws Exception {
+    treeRootHolder.setRoot(PROJECT_COMPONENT);
+  }
+
+  @Test
+  public void no_event_if_no_current_ALERT_STATUS_measure() {
+    when(measureRepository.findCurrent(PROJECT_COMPONENT, ALERT_STATUS)).thenReturn(Optional.<BatchReport.Measure>absent());
+
+    underTest.execute();
+
+    verify(measureRepository).findCurrent(PROJECT_COMPONENT, ALERT_STATUS);
+    verifyNoMoreInteractions(measureRepository, eventRepository);
+  }
+
+  @Test
+  public void no_event_created_if_current_ALTER_STATUS_measure_is_null() {
+    when(measureRepository.findCurrent(PROJECT_COMPONENT, ALERT_STATUS)).thenReturn(Optional.of(BatchReport.Measure.newBuilder().build()));
+
+    underTest.execute();
+
+    verify(measureRepository).findCurrent(PROJECT_COMPONENT, ALERT_STATUS);
+    verifyNoMoreInteractions(measureRepository, eventRepository);
+  }
+
+  @Test
+  public void no_event_created_if_current_ALTER_STATUS_measure_is_unsupported_value() {
+    when(measureRepository.findCurrent(PROJECT_COMPONENT, ALERT_STATUS)).thenReturn(Optional.of(BatchReport.Measure.newBuilder().setAlertStatus(INVALID_ALERT_STATUS).build()));
+
+    underTest.execute();
+
+    verify(measureRepository).findCurrent(PROJECT_COMPONENT, ALERT_STATUS);
+    verifyNoMoreInteractions(measureRepository, eventRepository);
+  }
+
+  @Test
+  public void no_event_created_if_no_past_ALTER_STATUS_and_current_is_OK() {
+    String alertStatus = "OK";
+
+    when(measureRepository.findCurrent(PROJECT_COMPONENT, ALERT_STATUS)).thenReturn(createBatchReportMeasure(alertStatus, null));
+    when(measureRepository.findPrevious(PROJECT_COMPONENT, ALERT_STATUS)).thenReturn(Optional.<MeasureDto>absent());
+
+    underTest.execute();
+
+    verify(measureRepository).findCurrent(PROJECT_COMPONENT, ALERT_STATUS);
+    verify(measureRepository).findPrevious(PROJECT_COMPONENT, ALERT_STATUS);
+    verifyNoMoreInteractions(measureRepository, eventRepository);
+  }
+
+  @Test
+  public void event_created_if_no_past_ALTER_STATUS_and_current_is_WARN() {
+    verify_event_created_if_no_past_ALTER_STATUS("WARN", "Orange", null);
+  }
+
+  @Test
+  public void event_created_if_past_ALTER_STATUS_and_current_is_ERROR() {
+    verify_event_created_if_no_past_ALTER_STATUS("ERROR", "Red", null);
+  }
+
+  @Test
+  public void event_created_if_past_ALTER_STATUS_has_no_alertStatus_and_current_is_ERROR() {
+    verify_event_created_if_no_past_ALTER_STATUS("ERROR", "Red", new MeasureDto());
+  }
+
+  @Test
+  public void event_created_if_past_ALTER_STATUS_has_no_alertStatus_and_current_is_WARN() {
+    verify_event_created_if_no_past_ALTER_STATUS("WARN", "Orange", new MeasureDto());
+  }
+
+  @Test
+  public void event_created_if_past_ALTER_STATUS_has_invalid_alertStatus_and_current_is_ERROR() {
+    verify_event_created_if_no_past_ALTER_STATUS("ERROR", "Red", new MeasureDto().setAlertStatus(INVALID_ALERT_STATUS));
+  }
+
+  @Test
+  public void event_created_if_past_ALTER_STATUS_has_invalid_alertStatus_and_current_is_WARN() {
+    verify_event_created_if_no_past_ALTER_STATUS("WARN", "Orange", new MeasureDto().setAlertStatus(INVALID_ALERT_STATUS));
+  }
+
+  private void verify_event_created_if_no_past_ALTER_STATUS(String currentAlterStatus, String expectedEventName, @Nullable MeasureDto measureDto) {
+    when(measureRepository.findCurrent(PROJECT_COMPONENT, ALERT_STATUS)).thenReturn(createBatchReportMeasure(currentAlterStatus, ALERT_TEXT));
+    when(measureRepository.findPrevious(PROJECT_COMPONENT, ALERT_STATUS)).thenReturn(Optional.fromNullable(measureDto));
+
+    underTest.execute();
+
+    verify(measureRepository).findCurrent(PROJECT_COMPONENT, ALERT_STATUS);
+    verify(measureRepository).findPrevious(PROJECT_COMPONENT, ALERT_STATUS);
+    verify(eventRepository).add(eq(PROJECT_COMPONENT), eventArgumentCaptor.capture());
+    verifyNoMoreInteractions(measureRepository, eventRepository);
+
+    Event event = eventArgumentCaptor.getValue();
+    assertThat(event.getCategory()).isEqualTo(Event.Category.ALERT);
+    assertThat(event.getName()).isEqualTo(expectedEventName);
+    assertThat(event.getDescription()).isEqualTo(ALERT_TEXT);
+    assertThat(event.getData()).isNull();
+  }
+
+  @Test
+  public void no_event_created_if_past_ALTER_STATUS_but_status_is_the_same() {
+    String alertStatus = "OK";
+
+    when(measureRepository.findCurrent(PROJECT_COMPONENT, ALERT_STATUS)).thenReturn(createBatchReportMeasure(alertStatus, ALERT_TEXT));
+    when(measureRepository.findPrevious(PROJECT_COMPONENT, ALERT_STATUS)).thenReturn(Optional.of(new MeasureDto().setAlertStatus(alertStatus)));
+
+    underTest.execute();
+
+    verify(measureRepository).findCurrent(PROJECT_COMPONENT, ALERT_STATUS);
+    verify(measureRepository).findPrevious(PROJECT_COMPONENT, ALERT_STATUS);
+    verifyNoMoreInteractions(measureRepository, eventRepository);
+  }
+
+  @Test
+  public void event_created_if_past_ALTER_STATUS_exists_and_status_has_changed() {
+    verify_event_created_if_past_ALTER_STATUS_exists_and_status_has_changed("OK", "WARN", "Orange (was Green)");
+    verify_event_created_if_past_ALTER_STATUS_exists_and_status_has_changed("OK", "ERROR", "Red (was Green)");
+    verify_event_created_if_past_ALTER_STATUS_exists_and_status_has_changed("WARN", "OK", "Green (was Orange)");
+    verify_event_created_if_past_ALTER_STATUS_exists_and_status_has_changed("WARN", "ERROR", "Red (was Orange)");
+    verify_event_created_if_past_ALTER_STATUS_exists_and_status_has_changed("ERROR", "OK", "Green (was Red)");
+    verify_event_created_if_past_ALTER_STATUS_exists_and_status_has_changed("ERROR", "WARN", "Orange (was Red)");
+  }
+
+  private void verify_event_created_if_past_ALTER_STATUS_exists_and_status_has_changed(String previousAlterStatus, String newAlertStatus, String expectedEventName) {
+    when(measureRepository.findCurrent(PROJECT_COMPONENT, ALERT_STATUS)).thenReturn(createBatchReportMeasure(newAlertStatus, ALERT_TEXT));
+    when(measureRepository.findPrevious(PROJECT_COMPONENT, ALERT_STATUS)).thenReturn(Optional.of(new MeasureDto().setAlertStatus(previousAlterStatus)));
+
+    underTest.execute();
+
+    verify(measureRepository).findCurrent(PROJECT_COMPONENT, ALERT_STATUS);
+    verify(measureRepository).findPrevious(PROJECT_COMPONENT, ALERT_STATUS);
+    verify(eventRepository).add(eq(PROJECT_COMPONENT), eventArgumentCaptor.capture());
+    verifyNoMoreInteractions(measureRepository, eventRepository);
+
+    Event event = eventArgumentCaptor.getValue();
+    assertThat(event.getCategory()).isEqualTo(Event.Category.ALERT);
+    assertThat(event.getName()).isEqualTo(expectedEventName);
+    assertThat(event.getDescription()).isEqualTo(ALERT_TEXT);
+    assertThat(event.getData()).isNull();
+
+    reset(measureRepository, eventRepository);
+  }
+
+  private static Optional<BatchReport.Measure> createBatchReportMeasure(String alertStatus, @Nullable String alertText) {
+    BatchReport.Measure.Builder builder = BatchReport.Measure.newBuilder().setAlertStatus(alertStatus);
+    if (alertText != null) {
+      builder.setAlertText(alertText);
+    }
+    return Optional.of(builder.build());
+  }
+}