aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>2015-06-01 12:05:47 +0200
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>2015-06-04 14:36:03 +0200
commit0cf12ff5662b16ac1ec8b89c0354447e02672195 (patch)
treebb6cffcdd3f1bed84ea06253829cdb46a757d4dd
parenta79d08d705501371a782b7a34e536b155cea9232 (diff)
downloadsonarqube-0cf12ff5662b16ac1ec8b89c0354447e02672195.tar.gz
sonarqube-0cf12ff5662b16ac1ec8b89c0354447e02672195.zip
SONAR-6569 add step to compute Quality Gate events in CE
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/computation/step/ComputationSteps.java2
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/computation/step/QualityGateEventsStep.java153
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/computation/step/QualityGateEventsStepTest.java208
3 files changed, 362 insertions, 1 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/step/ComputationSteps.java b/server/sonar-server/src/main/java/org/sonar/server/computation/step/ComputationSteps.java
index 25190675f4c..42b74c6c27d 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/computation/step/ComputationSteps.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/computation/step/ComputationSteps.java
@@ -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
index 00000000000..2335b32b248
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/computation/step/QualityGateEventsStep.java
@@ -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
index 00000000000..f050d689a2c
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/computation/step/QualityGateEventsStepTest.java
@@ -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());
+ }
+}