From: Sébastien Lesaint Date: Mon, 1 Jun 2015 10:05:47 +0000 (+0200) Subject: SONAR-6569 add step to compute Quality Gate events in CE X-Git-Tag: 5.2-RC1~1634 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=0cf12ff5662b16ac1ec8b89c0354447e02672195;p=sonarqube.git SONAR-6569 add step to compute Quality Gate events in CE --- 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 currentStatus = measureRepository.findCurrent(project, CoreMetrics.ALERT_STATUS); + if (!currentStatus.isPresent()) { + return; + } + Optional 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 previousMeasure = measureRepository.findPrevious(project, CoreMetrics.ALERT_STATUS); + if (!previousMeasure.isPresent()) { + checkNewQualityGate(project, currentStatus, alertText); + return; + } + + Optional 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 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 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.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.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 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()); + } +}