From: Sébastien Lesaint Date: Thu, 14 Sep 2017 15:16:49 +0000 (+0200) Subject: SONAR-7024 add notification on Report Analysis failures X-Git-Tag: 6.6-RC1~130 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=02d777cc52029915afc48f04a22296d40a7195b7;p=sonarqube.git SONAR-7024 add notification on Report Analysis failures --- diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java index 3b17e7b545c..da99c929216 100644 --- a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java +++ b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java @@ -49,6 +49,7 @@ import org.sonar.ce.StandaloneCeDistributedInformation; import org.sonar.ce.cleaning.CeCleaningModule; import org.sonar.ce.db.ReadOnlyPropertiesDao; import org.sonar.ce.log.CeProcessLogging; +import org.sonar.ce.notification.ReportAnalysisFailureNotificationModule; import org.sonar.ce.platform.ComputeEngineExtensionInstaller; import org.sonar.ce.queue.CeQueueCleaner; import org.sonar.ce.queue.PurgeCeActivities; @@ -398,6 +399,7 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer { NotificationService.class, DefaultNotificationManager.class, EmailNotificationChannel.class, + ReportAnalysisFailureNotificationModule.class, // Tests TestIndexer.class, diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationExecutionListener.java b/server/sonar-ce/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationExecutionListener.java new file mode 100644 index 00000000000..4a4c92a95e8 --- /dev/null +++ b/server/sonar-ce/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationExecutionListener.java @@ -0,0 +1,107 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.notification; + +import javax.annotation.Nullable; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.resources.Scopes; +import org.sonar.api.utils.System2; +import org.sonar.ce.queue.CeTask; +import org.sonar.ce.queue.CeTaskResult; +import org.sonar.ce.taskprocessor.CeWorker; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.RowNotFoundException; +import org.sonar.db.ce.CeActivityDto; +import org.sonar.db.ce.CeTaskTypes; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.notification.NotificationService; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.lang.String.format; +import static java.util.Collections.singleton; + +public class ReportAnalysisFailureNotificationExecutionListener implements CeWorker.ExecutionListener { + private final NotificationService notificationService; + private final DbClient dbClient; + private final ReportAnalysisFailureNotificationSerializer taskFailureNotificationSerializer; + private final System2 system2; + + public ReportAnalysisFailureNotificationExecutionListener(NotificationService notificationService, DbClient dbClient, + ReportAnalysisFailureNotificationSerializer taskFailureNotificationSerializer, System2 system2) { + this.notificationService = notificationService; + this.dbClient = dbClient; + this.taskFailureNotificationSerializer = taskFailureNotificationSerializer; + this.system2 = system2; + } + + @Override + public void onStart(CeTask ceTask) { + // nothing to do + } + + @Override + public void onEnd(CeTask ceTask, CeActivityDto.Status status, @Nullable CeTaskResult taskResult, @Nullable Throwable error) { + if (status == CeActivityDto.Status.SUCCESS) { + return; + } + String projectUuid = ceTask.getComponentUuid(); + if (!CeTaskTypes.REPORT.equals(ceTask.getType()) || projectUuid == null) { + return; + } + + if (notificationService.hasProjectSubscribersForTypes(projectUuid, singleton(ReportAnalysisFailureNotification.TYPE))) { + try (DbSession dbSession = dbClient.openSession(false)) { + ComponentDto projectDto = dbClient.componentDao().selectOrFailByUuid(dbSession, projectUuid); + checkScopeAndQualifier(projectDto); + CeActivityDto ceActivityDto = dbClient.ceActivityDao().selectByUuid(dbSession, ceTask.getUuid()) + .orElseThrow(() -> new RowNotFoundException(format("CeActivity with uuid '%s' not found", ceTask.getUuid()))); + ReportAnalysisFailureNotification taskFailureNotification = buildNotification(ceActivityDto, projectDto, error); + notificationService.deliver(taskFailureNotificationSerializer.toNotification(taskFailureNotification)); + } + } + } + + /** + * @throws IllegalArgumentException if specified {@link ComponentDto} is not a project. + */ + private static void checkScopeAndQualifier(ComponentDto projectDto) { + String scope = projectDto.scope(); + String qualifier = projectDto.qualifier(); + checkArgument( + scope.equals(Scopes.PROJECT) && qualifier.equals(Qualifiers.PROJECT), + "Component %s must be a project (scope=%s, qualifier=%s)", projectDto.uuid(), scope, qualifier); + } + + private ReportAnalysisFailureNotification buildNotification(CeActivityDto ceActivityDto, ComponentDto projectDto, @Nullable Throwable error) { + Long executedAt = ceActivityDto.getExecutedAt(); + return new ReportAnalysisFailureNotification( + new ReportAnalysisFailureNotification.Project( + projectDto.uuid(), + projectDto.getKey(), + projectDto.name(), + projectDto.getBranch()), + new ReportAnalysisFailureNotification.Task( + ceActivityDto.getUuid(), + ceActivityDto.getSubmittedAt(), + executedAt == null ? system2.now() : executedAt), + error == null ? null : error.getMessage()); + } +} diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/notification/package-info.java b/server/sonar-ce/src/main/java/org/sonar/ce/notification/package-info.java new file mode 100644 index 00000000000..da3dc0bb31e --- /dev/null +++ b/server/sonar-ce/src/main/java/org/sonar/ce/notification/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.ce.notification; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/taskprocessor/CeTaskProcessorModule.java b/server/sonar-ce/src/main/java/org/sonar/ce/taskprocessor/CeTaskProcessorModule.java index ece8e9d6925..8cfde5df5d3 100644 --- a/server/sonar-ce/src/main/java/org/sonar/ce/taskprocessor/CeTaskProcessorModule.java +++ b/server/sonar-ce/src/main/java/org/sonar/ce/taskprocessor/CeTaskProcessorModule.java @@ -19,6 +19,7 @@ */ package org.sonar.ce.taskprocessor; +import org.sonar.ce.notification.ReportAnalysisFailureNotificationExecutionListener; import org.sonar.core.platform.Module; public class CeTaskProcessorModule extends Module { @@ -27,6 +28,7 @@ public class CeTaskProcessorModule extends Module { add( CeTaskProcessorRepositoryImpl.class, CeLoggingWorkerExecutionListener.class, + ReportAnalysisFailureNotificationExecutionListener.class, CeWorkerFactoryImpl.class, EnabledCeWorkerControllerImpl.class, CeProcessingSchedulerExecutorServiceImpl.class, diff --git a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java index 74fed0f8a2e..fefec4144f9 100644 --- a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java +++ b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java @@ -129,13 +129,14 @@ public class ComputeEngineContainerImplTest { assertThat(picoContainer.getComponentAdapters()) .hasSize( CONTAINER_ITSELF - + 74 // level 4 + + 75 // level 4 + 4 // content of CeConfigurationModule + 7 // content of CeQueueModule + 4 // content of CeHttpModule + 3 // content of CeTaskCommonsModule + 4 // content of ProjectAnalysisTaskModule - + 6 // content of CeTaskProcessorModule + + 7 // content of CeTaskProcessorModule + + 4 // content of ReportAnalysisFailureNotificationModule + 3 // CeCleaningModule + its content + 1 // CeDistributedInformation ); diff --git a/server/sonar-ce/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationExecutionListenerTest.java b/server/sonar-ce/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationExecutionListenerTest.java new file mode 100644 index 00000000000..2c305d3c5dc --- /dev/null +++ b/server/sonar-ce/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationExecutionListenerTest.java @@ -0,0 +1,318 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.notification; + +import java.util.Arrays; +import java.util.Random; +import javax.annotation.Nullable; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.ArgumentCaptor; +import org.sonar.api.notifications.Notification; +import org.sonar.api.utils.System2; +import org.sonar.ce.queue.CeTask; +import org.sonar.ce.queue.CeTaskResult; +import org.sonar.db.DbClient; +import org.sonar.db.DbTester; +import org.sonar.db.RowNotFoundException; +import org.sonar.db.ce.CeActivityDto; +import org.sonar.db.ce.CeQueueDto; +import org.sonar.db.ce.CeTaskTypes; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ComponentTesting; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.db.organization.OrganizationTesting; +import org.sonar.server.notification.NotificationService; + +import static java.util.Collections.singleton; +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.Matchers.any; +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.db.component.ComponentTesting.newDirectory; +import static org.sonar.db.component.ComponentTesting.newModuleDto; + +public class ReportAnalysisFailureNotificationExecutionListenerTest { + @Rule + public DbTester dbTester = DbTester.create(System2.INSTANCE); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private final Random random = new Random(); + private DbClient dbClient = dbTester.getDbClient(); + private NotificationService notificationService = mock(NotificationService.class); + private ReportAnalysisFailureNotificationSerializer serializer = mock(ReportAnalysisFailureNotificationSerializer.class); + private System2 system2 = mock(System2.class); + private DbClient dbClientMock = mock(DbClient.class); + private CeTask ceTaskMock = mock(CeTask.class); + private Throwable throwableMock = mock(Throwable.class); + private CeTaskResult ceTaskResultMock = mock(CeTaskResult.class); + private ReportAnalysisFailureNotificationExecutionListener fullMockedUnderTest = new ReportAnalysisFailureNotificationExecutionListener( + notificationService, dbClientMock, serializer, system2); + private ReportAnalysisFailureNotificationExecutionListener underTest = new ReportAnalysisFailureNotificationExecutionListener( + notificationService, dbClient, serializer, system2); + + @Before + public void setUp() throws Exception { + + } + + @Test + public void onStart_has_no_effect() { + CeTask mockedCeTask = mock(CeTask.class); + + fullMockedUnderTest.onStart(mockedCeTask); + + verifyZeroInteractions(mockedCeTask, notificationService, dbClientMock, serializer, system2); + } + + @Test + public void onEnd_has_no_effect_if_status_is_SUCCESS() { + fullMockedUnderTest.onEnd(ceTaskMock, CeActivityDto.Status.SUCCESS, ceTaskResultMock, throwableMock); + + verifyZeroInteractions(ceTaskMock, ceTaskResultMock, throwableMock, notificationService, dbClientMock, serializer, system2); + } + + @Test + public void onEnd_has_no_effect_if_CeTask_type_is_not_report() { + when(ceTaskMock.getType()).thenReturn(randomAlphanumeric(12)); + + fullMockedUnderTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, ceTaskResultMock, throwableMock); + + verifyZeroInteractions(ceTaskResultMock, throwableMock, notificationService, dbClientMock, serializer, system2); + } + + @Test + public void onEnd_has_no_effect_if_CeTask_has_no_component_uuid() { + when(ceTaskMock.getType()).thenReturn(CeTaskTypes.REPORT); + + fullMockedUnderTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, ceTaskResultMock, throwableMock); + + verifyZeroInteractions(ceTaskResultMock, throwableMock, notificationService, dbClientMock, serializer, system2); + } + + @Test + public void onEnd_has_no_effect_if_there_is_no_subscriber_for_ReportAnalysisFailureNotification_type() { + String componentUuid = randomAlphanumeric(6); + when(ceTaskMock.getType()).thenReturn(CeTaskTypes.REPORT); + when(ceTaskMock.getComponentUuid()).thenReturn(componentUuid); + when(notificationService.hasProjectSubscribersForTypes(componentUuid, singleton(ReportAnalysisFailureNotification.TYPE))) + .thenReturn(false); + + fullMockedUnderTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, ceTaskResultMock, throwableMock); + + verifyZeroInteractions(ceTaskResultMock, throwableMock, dbClientMock, serializer, system2); + } + + @Test + public void onEnd_fails_with_RowNotFoundException_if_component_does_not_exist_in_DB() { + String componentUuid = randomAlphanumeric(6); + when(ceTaskMock.getType()).thenReturn(CeTaskTypes.REPORT); + when(ceTaskMock.getComponentUuid()).thenReturn(componentUuid); + when(notificationService.hasProjectSubscribersForTypes(componentUuid, singleton(ReportAnalysisFailureNotification.TYPE))) + .thenReturn(true); + + expectedException.expect(RowNotFoundException.class); + expectedException.expectMessage("Component with uuid '" + componentUuid + "' not found"); + + underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, ceTaskResultMock, throwableMock); + } + + @Test + public void onEnd_fails_with_IAE_if_component_is_not_a_project() { + when(ceTaskMock.getType()).thenReturn(CeTaskTypes.REPORT); + OrganizationDto organization = OrganizationTesting.newOrganizationDto(); + ComponentDto project = dbTester.components().insertPrivateProject(); + ComponentDto module = dbTester.components().insertComponent(newModuleDto(project)); + ComponentDto directory = dbTester.components().insertComponent(newDirectory(module, randomAlphanumeric(12))); + ComponentDto file = dbTester.components().insertComponent(ComponentTesting.newFileDto(project)); + ComponentDto view = dbTester.components().insertComponent(ComponentTesting.newView(organization)); + ComponentDto subView = dbTester.components().insertComponent(ComponentTesting.newSubView(view)); + ComponentDto projectCopy = dbTester.components().insertComponent(ComponentTesting.newProjectCopy(project, subView)); + ComponentDto application = dbTester.components().insertComponent(ComponentTesting.newApplication(organization)); + + Arrays.asList(module, directory, file, view, subView, projectCopy, application) + .forEach(component -> { + try { + when(ceTaskMock.getComponentUuid()).thenReturn(component.uuid()); + when(notificationService.hasProjectSubscribersForTypes(component.uuid(), singleton(ReportAnalysisFailureNotification.TYPE))) + .thenReturn(true); + + underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, ceTaskResultMock, throwableMock); + + fail("An IllegalArgumentException should have been thrown for component " + component); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).isEqualTo(String.format( + "Component %s must be a project (scope=%s, qualifier=%s)", component.uuid(), component.scope(), component.qualifier())); + } + }); + } + + @Test + public void onEnd_fails_with_RowNotFoundException_if_activity_for_task_does_not_exist_in_DB() { + String componentUuid = randomAlphanumeric(6); + String taskUuid = randomAlphanumeric(6); + when(ceTaskMock.getType()).thenReturn(CeTaskTypes.REPORT); + when(ceTaskMock.getUuid()).thenReturn(taskUuid); + when(ceTaskMock.getComponentUuid()).thenReturn(componentUuid); + when(notificationService.hasProjectSubscribersForTypes(componentUuid, singleton(ReportAnalysisFailureNotification.TYPE))) + .thenReturn(true); + dbTester.components().insertPrivateProject(s -> s.setUuid(componentUuid)); + + expectedException.expect(RowNotFoundException.class); + expectedException.expectMessage("CeActivity with uuid '" + taskUuid + "' not found"); + + underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, ceTaskResultMock, throwableMock); + } + + @Test + public void onEnd_creates_notification_with_data_from_activity_and_project_and_deliver_it() { + String taskUuid = randomAlphanumeric(12); + int createdAt = random.nextInt(999_999); + long executedAt = random.nextInt(999_999); + ComponentDto project = initMocksToPassConditions(taskUuid, createdAt, executedAt); + Notification notificationMock = mockSerializer(); + + underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, ceTaskResultMock, throwableMock); + + ArgumentCaptor notificationCaptor = verifyAndCaptureSerializedNotification(); + verify(notificationService).deliver(same(notificationMock)); + + ReportAnalysisFailureNotification reportAnalysisFailureNotification = notificationCaptor.getValue(); + + ReportAnalysisFailureNotification.Project notificationProject = reportAnalysisFailureNotification.getProject(); + assertThat(notificationProject.getName()).isEqualTo(project.name()); + assertThat(notificationProject.getKey()).isEqualTo(project.getKey()); + assertThat(notificationProject.getUuid()).isEqualTo(project.uuid()); + assertThat(notificationProject.getBranchName()).isEqualTo(project.getBranch()); + ReportAnalysisFailureNotification.Task notificationTask = reportAnalysisFailureNotification.getTask(); + assertThat(notificationTask.getUuid()).isEqualTo(taskUuid); + assertThat(notificationTask.getCreatedAt()).isEqualTo(createdAt); + assertThat(notificationTask.getFailedAt()).isEqualTo(executedAt); + } + + @Test + public void onEnd_creates_notification_with_error_message_from_Throwable_argument_message() { + initMocksToPassConditions(randomAlphanumeric(12), random.nextInt(999_999), (long) random.nextInt(999_999)); + String message = randomAlphanumeric(66); + when(throwableMock.getMessage()).thenReturn(message); + + underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, ceTaskResultMock, throwableMock); + + ArgumentCaptor notificationCaptor = verifyAndCaptureSerializedNotification(); + + ReportAnalysisFailureNotification reportAnalysisFailureNotification = notificationCaptor.getValue(); + assertThat(reportAnalysisFailureNotification.getErrorMessage()).isEqualTo(message); + } + + @Test + public void onEnd_creates_notification_with_null_error_message_if_Throwable_is_null() { + String taskUuid = randomAlphanumeric(12); + initMocksToPassConditions(taskUuid, random.nextInt(999_999), (long) random.nextInt(999_999)); + Notification notificationMock = mockSerializer(); + + underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, ceTaskResultMock, null); + + verify(notificationService).deliver(same(notificationMock)); + ArgumentCaptor notificationCaptor = verifyAndCaptureSerializedNotification(); + + ReportAnalysisFailureNotification reportAnalysisFailureNotification = notificationCaptor.getValue(); + assertThat(reportAnalysisFailureNotification.getErrorMessage()).isNull(); + } + + @Test + public void onEnd_ignores_null_CeTaskResult_argument() { + String taskUuid = randomAlphanumeric(12); + initMocksToPassConditions(taskUuid, random.nextInt(999_999), (long) random.nextInt(999_999)); + Notification notificationMock = mockSerializer(); + + underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, null, null); + + verify(notificationService).deliver(same(notificationMock)); + } + + @Test + public void onEnd_ignores_CeTaskResult_argument() { + String taskUuid = randomAlphanumeric(12); + initMocksToPassConditions(taskUuid, random.nextInt(999_999), (long) random.nextInt(999_999)); + Notification notificationMock = mockSerializer(); + + underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, ceTaskResultMock, null); + + verify(notificationService).deliver(same(notificationMock)); + verifyZeroInteractions(ceTaskResultMock); + } + + @Test + public void onEnd_uses_system_data_as_failedAt_if_task_has_no_executedAt() { + String taskUuid = randomAlphanumeric(12); + initMocksToPassConditions(taskUuid, random.nextInt(999_999), null); + long now = random.nextInt(999_999); + when(system2.now()).thenReturn(now); + Notification notificationMock = mockSerializer(); + + underTest.onEnd(ceTaskMock, CeActivityDto.Status.FAILED, ceTaskResultMock, null); + + verify(notificationService).deliver(same(notificationMock)); + ArgumentCaptor notificationCaptor = verifyAndCaptureSerializedNotification(); + assertThat(notificationCaptor.getValue().getTask().getFailedAt()).isEqualTo(now); + } + + private Notification mockSerializer() { + Notification notificationMock = mock(Notification.class); + when(serializer.toNotification(any(ReportAnalysisFailureNotification.class))).thenReturn(notificationMock); + return notificationMock; + } + + private ComponentDto initMocksToPassConditions(String taskUuid, int createdAt, @Nullable Long executedAt) { + ComponentDto project = random.nextBoolean() ? dbTester.components().insertPrivateProject() : dbTester.components().insertPublicProject(); + when(ceTaskMock.getType()).thenReturn(CeTaskTypes.REPORT); + when(ceTaskMock.getComponentUuid()).thenReturn(project.uuid()); + when(ceTaskMock.getUuid()).thenReturn(taskUuid); + when(notificationService.hasProjectSubscribersForTypes(project.uuid(), singleton(ReportAnalysisFailureNotification.TYPE))) + .thenReturn(true); + insertActivityDto(taskUuid, createdAt, executedAt, project); + return project; + } + + private void insertActivityDto(String taskUuid, int createdAt, @Nullable Long executedAt, ComponentDto project) { + dbClient.ceActivityDao().insert(dbTester.getSession(), new CeActivityDto(new CeQueueDto() + .setUuid(taskUuid) + .setTaskType(CeTaskTypes.REPORT) + .setComponentUuid(project.uuid()) + .setCreatedAt(createdAt)) + .setExecutedAt(executedAt) + .setStatus(CeActivityDto.Status.FAILED)); + dbTester.getSession().commit(); + } + + private ArgumentCaptor verifyAndCaptureSerializedNotification() { + ArgumentCaptor notificationCaptor = ArgumentCaptor.forClass(ReportAnalysisFailureNotification.class); + verify(serializer).toNotification(notificationCaptor.capture()); + return notificationCaptor; + } +} diff --git a/server/sonar-ce/src/test/java/org/sonar/ce/taskprocessor/CeTaskProcessorModuleTest.java b/server/sonar-ce/src/test/java/org/sonar/ce/taskprocessor/CeTaskProcessorModuleTest.java index ae8f1faacf8..73a65dfead7 100644 --- a/server/sonar-ce/src/test/java/org/sonar/ce/taskprocessor/CeTaskProcessorModuleTest.java +++ b/server/sonar-ce/src/test/java/org/sonar/ce/taskprocessor/CeTaskProcessorModuleTest.java @@ -21,6 +21,7 @@ package org.sonar.ce.taskprocessor; import org.junit.Test; import org.picocontainer.ComponentAdapter; +import org.sonar.ce.notification.ReportAnalysisFailureNotificationExecutionListener; import org.sonar.core.platform.ComponentContainer; import static org.assertj.core.api.Assertions.assertThat; @@ -39,4 +40,16 @@ public class CeTaskProcessorModuleTest { .map(ComponentAdapter::getComponentImplementation)) .contains(CeLoggingWorkerExecutionListener.class); } + + @Test + public void defines_ExecutionListener_for_report_processing_failure_notifications() { + ComponentContainer container = new ComponentContainer(); + + underTest.configure(container); + + assertThat(container.getPicoContainer().getComponentAdapters(CeWorker.ExecutionListener.class) + .stream() + .map(ComponentAdapter::getComponentImplementation)) + .contains(ReportAnalysisFailureNotificationExecutionListener.class); + } } diff --git a/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotification.java b/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotification.java new file mode 100644 index 00000000000..0b424db14d3 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotification.java @@ -0,0 +1,107 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.notification; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; + +import static java.util.Objects.requireNonNull; + +public class ReportAnalysisFailureNotification { + public static final String TYPE = "ce-report-task-failure"; + + private final Project project; + private final Task task; + private final String errorMessage; + + public ReportAnalysisFailureNotification(Project project, Task task, @Nullable String errorMessage) { + this.project = requireNonNull(project, "project can't be null"); + this.task = requireNonNull(task, "task can't be null"); + this.errorMessage = errorMessage; + } + + public Project getProject() { + return project; + } + + public Task getTask() { + return task; + } + + @CheckForNull + public String getErrorMessage() { + return errorMessage; + } + + public static final class Project { + private final String uuid; + private final String key; + private final String name; + private final String branchName; + + public Project(String uuid, String key, String name, @Nullable String branchName) { + this.uuid = requireNonNull(uuid, "uuid can't be null"); + this.key = requireNonNull(key, "key can't be null"); + this.name = requireNonNull(name, "name can't be null"); + this.branchName = branchName; + } + + public String getUuid() { + return uuid; + } + + public String getKey() { + return key; + } + + public String getName() { + return name; + } + + @CheckForNull + public String getBranchName() { + return branchName; + } + } + + public static final class Task { + private final String uuid; + private final long createdAt; + private final long failedAt; + + public Task(String uuid, long createdAt, long failedAt) { + this.uuid = requireNonNull(uuid, "uuid can't be null"); + this.createdAt = createdAt; + this.failedAt = failedAt; + } + + public String getUuid() { + return uuid; + } + + public long getCreatedAt() { + return createdAt; + } + + public long getFailedAt() { + return failedAt; + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationDispatcher.java b/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationDispatcher.java new file mode 100644 index 00000000000..574bb17e594 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationDispatcher.java @@ -0,0 +1,65 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.notification; + +import com.google.common.collect.Multimap; +import java.util.Collection; +import java.util.Map; +import org.sonar.api.notifications.Notification; +import org.sonar.api.notifications.NotificationChannel; +import org.sonar.server.notification.NotificationDispatcher; +import org.sonar.server.notification.NotificationDispatcherMetadata; +import org.sonar.server.notification.NotificationManager; + +public class ReportAnalysisFailureNotificationDispatcher extends NotificationDispatcher { + + public static final String KEY = "CeReportTaskFailure"; + + private final NotificationManager manager; + + public ReportAnalysisFailureNotificationDispatcher(NotificationManager manager) { + super(ReportAnalysisFailureNotification.TYPE); + this.manager = manager; + } + + public static NotificationDispatcherMetadata newMetadata() { + return NotificationDispatcherMetadata.create(KEY) + .setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(true)) + .setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true)); + } + + @Override + public String getKey() { + return KEY; + } + + @Override + public void dispatch(Notification notification, Context context) { + String projectUuid = notification.getFieldValue("project.uuid"); + Multimap subscribedRecipients = manager.findSubscribedRecipientsForDispatcher(this, projectUuid); + + for (Map.Entry> channelsByRecipients : subscribedRecipients.asMap().entrySet()) { + String userLogin = channelsByRecipients.getKey(); + for (NotificationChannel channel : channelsByRecipients.getValue()) { + context.addUser(userLogin, channel); + } + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationEmailTemplate.java b/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationEmailTemplate.java new file mode 100644 index 00000000000..22e16fdffad --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationEmailTemplate.java @@ -0,0 +1,100 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.notification; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import org.sonar.api.config.EmailSettings; +import org.sonar.api.notifications.Notification; +import org.sonar.plugins.emailnotifications.api.EmailMessage; +import org.sonar.plugins.emailnotifications.api.EmailTemplate; + +import static org.sonar.api.utils.DateUtils.formatDateTime; + +public class ReportAnalysisFailureNotificationEmailTemplate extends EmailTemplate { + private static final char LINE_RETURN = '\n'; + private static final char TAB = '\t'; + + private final ReportAnalysisFailureNotificationSerializer serializer; + protected final EmailSettings settings; + + public ReportAnalysisFailureNotificationEmailTemplate(ReportAnalysisFailureNotificationSerializer serializer, EmailSettings settings) { + this.serializer = serializer; + this.settings = settings; + } + + @Override + public EmailMessage format(Notification notification) { + if (!ReportAnalysisFailureNotification.TYPE.equals(notification.getType())) { + return null; + } + + ReportAnalysisFailureNotification taskFailureNotification = serializer.fromNotification(notification); + String projectUuid = taskFailureNotification.getProject().getUuid(); + String projectFullName = computeProjectFullName(taskFailureNotification.getProject()); + + return new EmailMessage() + .setMessageId(notification.getType() + "/" + projectUuid) + .setSubject(subject(projectFullName)) + .setMessage(message(projectFullName, taskFailureNotification)); + } + + private static String computeProjectFullName(ReportAnalysisFailureNotification.Project project) { + String branchName = project.getBranchName(); + if (branchName != null) { + return String.format("%s (%s)", project.getName(), branchName); + } + return project.getName(); + } + + private static String subject(String projectFullName) { + return String.format("%s: Background task in failure", projectFullName); + } + + private String message(String projectFullName, ReportAnalysisFailureNotification taskFailureNotification) { + ReportAnalysisFailureNotification.Project project = taskFailureNotification.getProject(); + ReportAnalysisFailureNotification.Task task = taskFailureNotification.getTask(); + + StringBuilder res = new StringBuilder(); + res.append("Project:").append(TAB).append(projectFullName).append(LINE_RETURN); + res.append("Background task:").append(TAB).append(task.getUuid()).append(LINE_RETURN); + res.append("Submission time:").append(TAB).append(formatDateTime(task.getCreatedAt())).append(LINE_RETURN); + res.append("Failure time:").append(TAB).append(formatDateTime(task.getFailedAt())).append(LINE_RETURN); + + String errorMessage = taskFailureNotification.getErrorMessage(); + if (errorMessage != null) { + res.append(LINE_RETURN); + res.append("Error message:").append(TAB).append(errorMessage).append(LINE_RETURN); + } + + res.append(LINE_RETURN); + res.append("More details at: ").append(String.format("%s/project/background_tasks?id=%s", settings.getServerBaseURL(), encode(project.getKey()))); + + return res.toString(); + } + + private static String encode(String toEncode) { + try { + return URLEncoder.encode(toEncode, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException("Encoding not supported", e); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationModule.java b/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationModule.java new file mode 100644 index 00000000000..8c86320fd1b --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationModule.java @@ -0,0 +1,33 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.notification; + +import org.sonar.core.platform.Module; + +public class ReportAnalysisFailureNotificationModule extends Module { + @Override + protected void configureModule() { + add( + ReportAnalysisFailureNotificationDispatcher.class, + ReportAnalysisFailureNotificationDispatcher.newMetadata(), + ReportAnalysisFailureNotificationSerializerImpl.class, + ReportAnalysisFailureNotificationEmailTemplate.class); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationSerializer.java b/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationSerializer.java new file mode 100644 index 00000000000..9409692ba5e --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationSerializer.java @@ -0,0 +1,28 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.notification; + +import org.sonar.api.notifications.Notification; + +public interface ReportAnalysisFailureNotificationSerializer { + Notification toNotification(ReportAnalysisFailureNotification reportAnalysisFailureNotification); + + ReportAnalysisFailureNotification fromNotification(Notification notification); +} diff --git a/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationSerializerImpl.java b/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationSerializerImpl.java new file mode 100644 index 00000000000..022568aeab2 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationSerializerImpl.java @@ -0,0 +1,63 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.notification; + +import org.sonar.api.notifications.Notification; + +import static java.lang.String.valueOf; + +public class ReportAnalysisFailureNotificationSerializerImpl implements ReportAnalysisFailureNotificationSerializer { + private static final String FIELD_PROJECT_UUID = "project.uuid"; + private static final String FIELD_PROJECT_KEY = "project.key"; + private static final String FIELD_PROJECT_NAME = "project.name"; + private static final String FIELD_PROJECT_BRANCH = "project.branchName"; + private static final String FIELD_TASK_UUID = "task.uuid"; + private static final String FIELD_TASK_CREATED_AT = "task.createdAt"; + private static final String FIELD_TASK_FAILED_AT = "task.failedAt"; + private static final String FIELD_ERROR_MESSAGE = "error.message"; + + @Override + public Notification toNotification(ReportAnalysisFailureNotification reportAnalysisFailureNotification) { + return new Notification(ReportAnalysisFailureNotification.TYPE) + .setFieldValue(FIELD_PROJECT_UUID, reportAnalysisFailureNotification.getProject().getUuid()) + .setFieldValue(FIELD_PROJECT_KEY, reportAnalysisFailureNotification.getProject().getKey()) + .setFieldValue(FIELD_PROJECT_NAME, reportAnalysisFailureNotification.getProject().getName()) + .setFieldValue(FIELD_PROJECT_BRANCH, reportAnalysisFailureNotification.getProject().getBranchName()) + .setFieldValue(FIELD_TASK_UUID, reportAnalysisFailureNotification.getTask().getUuid()) + .setFieldValue(FIELD_TASK_CREATED_AT, valueOf(reportAnalysisFailureNotification.getTask().getCreatedAt())) + .setFieldValue(FIELD_TASK_FAILED_AT, valueOf(reportAnalysisFailureNotification.getTask().getFailedAt())) + .setFieldValue(FIELD_ERROR_MESSAGE, reportAnalysisFailureNotification.getErrorMessage()); + } + + @Override + public ReportAnalysisFailureNotification fromNotification(Notification notification) { + return new ReportAnalysisFailureNotification( + new ReportAnalysisFailureNotification.Project( + notification.getFieldValue(FIELD_PROJECT_UUID), + notification.getFieldValue(FIELD_PROJECT_KEY), + notification.getFieldValue(FIELD_PROJECT_NAME), + notification.getFieldValue(FIELD_PROJECT_BRANCH)), + new ReportAnalysisFailureNotification.Task( + notification.getFieldValue(FIELD_TASK_UUID), + Long.valueOf(notification.getFieldValue(FIELD_TASK_CREATED_AT)), + Long.valueOf(notification.getFieldValue(FIELD_TASK_FAILED_AT))), + notification.getFieldValue(FIELD_ERROR_MESSAGE)); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/ce/notification/package-info.java b/server/sonar-server/src/main/java/org/sonar/ce/notification/package-info.java new file mode 100644 index 00000000000..da3dc0bb31e --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/ce/notification/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.ce.notification; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java index 86d7c3b7610..1e9b0f22ddd 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java @@ -21,9 +21,7 @@ package org.sonar.server.computation.task.projectanalysis.container; import java.util.Arrays; import java.util.List; - import javax.annotation.Nullable; - import org.sonar.ce.organization.DefaultOrganizationLoader; import org.sonar.ce.queue.CeTask; import org.sonar.ce.settings.SettingsLoader; diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java index 0ef1562a489..fcbf8711124 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java @@ -30,6 +30,7 @@ import org.sonar.api.rules.AnnotationRuleParser; import org.sonar.api.rules.XMLRuleParser; import org.sonar.api.server.rule.RulesDefinitionXmlLoader; import org.sonar.ce.CeModule; +import org.sonar.ce.notification.ReportAnalysisFailureNotificationModule; import org.sonar.ce.settings.ProjectConfigurationFactory; import org.sonar.core.component.DefaultResourceTypes; import org.sonar.core.timemachine.Periods; @@ -520,6 +521,7 @@ public class PlatformLevel4 extends PlatformLevel { PrivilegedPluginsStopper.class, // Compute engine (must be after Views and Developer Cockpit) + ReportAnalysisFailureNotificationModule.class, CeModule.class, CeWsModule.class, diff --git a/server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationDispatcherTest.java b/server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationDispatcherTest.java new file mode 100644 index 00000000000..f842b17506f --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationDispatcherTest.java @@ -0,0 +1,116 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.notification; + +import com.google.common.collect.HashMultimap; +import org.junit.Test; +import org.sonar.api.notifications.Notification; +import org.sonar.api.notifications.NotificationChannel; +import org.sonar.server.notification.NotificationDispatcher; +import org.sonar.server.notification.NotificationDispatcherMetadata; +import org.sonar.server.notification.NotificationManager; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +public class ReportAnalysisFailureNotificationDispatcherTest { + private NotificationManager notificationManager = mock(NotificationManager.class); + private Notification notificationMock = mock(Notification.class); + private NotificationDispatcher.Context contextMock = mock(NotificationDispatcher.Context.class); + private ReportAnalysisFailureNotificationDispatcher underTest = new ReportAnalysisFailureNotificationDispatcher(notificationManager); + + @Test + public void dispatcher_defines_key() { + assertThat(underTest.getKey()).isNotEmpty(); + } + + @Test + public void newMetadata_indicates_enabled_global_and_project_level_notifications() { + NotificationDispatcherMetadata metadata = ReportAnalysisFailureNotificationDispatcher.newMetadata(); + + assertThat(metadata.getProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION)).isEqualTo("true"); + assertThat(metadata.getProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION)).isEqualTo("true"); + } + + @Test + public void performDispatch_has_no_effect_if_type_is_empty() { + when(notificationMock.getType()).thenReturn(""); + + underTest.performDispatch(notificationMock, contextMock); + + verify(notificationMock).getType(); + verifyNoMoreInteractions(notificationMock, contextMock); + } + + @Test + public void performDispatch_has_no_effect_if_type_is_not_ReportAnalysisFailureNotification_TYPE() { + when(notificationMock.getType()).thenReturn(randomAlphanumeric(6)); + + underTest.performDispatch(notificationMock, contextMock); + + verify(notificationMock).getType(); + verifyNoMoreInteractions(notificationMock, contextMock); + } + + @Test + public void performDispatch_adds_user_for_each_recipient_and_channel_for_the_component_uuid_in_the_notification() { + when(notificationMock.getType()).thenReturn(ReportAnalysisFailureNotification.TYPE); + String projectUuid = randomAlphanumeric(9); + when(notificationMock.getFieldValue("project.uuid")).thenReturn(projectUuid); + HashMultimap multimap = HashMultimap.create(); + String login1 = randomAlphanumeric(3); + String login2 = randomAlphanumeric(3); + NotificationChannel channel1 = mock(NotificationChannel.class); + NotificationChannel channel2 = mock(NotificationChannel.class); + NotificationChannel channel3 = mock(NotificationChannel.class); + multimap.put(login1, channel1); + multimap.put(login1, channel2); + multimap.put(login2, channel2); + multimap.put(login2, channel3); + when(notificationManager.findSubscribedRecipientsForDispatcher(underTest, projectUuid)) + .thenReturn(multimap); + + underTest.performDispatch(notificationMock, contextMock); + + verify(contextMock).addUser(login1, channel1); + verify(contextMock).addUser(login1, channel2); + verify(contextMock).addUser(login2, channel2); + verify(contextMock).addUser(login2, channel3); + verifyNoMoreInteractions(contextMock); + } + + @Test + public void performDispatch_adds_no_user_if_notification_manager_returns_none() { + when(notificationMock.getType()).thenReturn(ReportAnalysisFailureNotification.TYPE); + String projectUuid = randomAlphanumeric(9); + when(notificationMock.getFieldValue("project.uuid")).thenReturn(projectUuid); + HashMultimap multimap = HashMultimap.create(); + when(notificationManager.findSubscribedRecipientsForDispatcher(underTest, projectUuid)) + .thenReturn(multimap); + + underTest.performDispatch(notificationMock, contextMock); + + verifyNoMoreInteractions(contextMock); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationEmailTemplateTest.java b/server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationEmailTemplateTest.java new file mode 100644 index 00000000000..6fdec2bc6c9 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationEmailTemplateTest.java @@ -0,0 +1,144 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.notification; + +import java.util.Random; +import org.apache.commons.lang.RandomStringUtils; +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.config.EmailSettings; +import org.sonar.api.notifications.Notification; +import org.sonar.api.utils.DateUtils; +import org.sonar.plugins.emailnotifications.api.EmailMessage; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ReportAnalysisFailureNotificationEmailTemplateTest { + private String serverUrl = RandomStringUtils.randomAlphanumeric(12); + private Notification notification = new Notification(ReportAnalysisFailureNotification.TYPE); + private Random random = new Random(); + private ReportAnalysisFailureNotification.Project projectNoBranch = new ReportAnalysisFailureNotification.Project( + randomAlphanumeric(2), + randomAlphanumeric(3), + randomAlphanumeric(4), + null); + private ReportAnalysisFailureNotification.Project projectWithBranch = new ReportAnalysisFailureNotification.Project( + randomAlphanumeric(6), + randomAlphanumeric(7), + randomAlphanumeric(8), + randomAlphanumeric(9)); + private ReportAnalysisFailureNotification.Task task = new ReportAnalysisFailureNotification.Task( + randomAlphanumeric(10), + random.nextInt(99), + random.nextInt(99)); + private String errorMessage = randomAlphanumeric(11); + + private ReportAnalysisFailureNotificationSerializer serializer = mock(ReportAnalysisFailureNotificationSerializer.class); + private EmailSettings emailSettings = mock(EmailSettings.class); + private ReportAnalysisFailureNotificationEmailTemplate underTest = new ReportAnalysisFailureNotificationEmailTemplate(serializer, emailSettings); + + @Before + public void setUp() throws Exception { + when(emailSettings.getServerBaseURL()).thenReturn(serverUrl); + } + + @Test + public void returns_null_if_notification_type_is_not_ReportAnalysisFailureNotification_TYPE() { + Notification notification = new Notification(RandomStringUtils.randomAlphanumeric(5)); + + EmailMessage emailMessage = underTest.format(notification); + + assertThat(emailMessage).isNull(); + } + + @Test + public void format_returns_email_with_subject_without_branch() { + when(serializer.fromNotification(notification)).thenReturn(new ReportAnalysisFailureNotification( + projectNoBranch, task, errorMessage)); + + EmailMessage emailMessage = underTest.format(notification); + + assertThat(emailMessage.getSubject()).isEqualTo(projectNoBranch.getName() + ": Background task in failure"); + } + + @Test + public void format_returns_email_with_subject_with_branch() { + when(serializer.fromNotification(notification)).thenReturn(new ReportAnalysisFailureNotification( + projectWithBranch, task, errorMessage)); + + EmailMessage emailMessage = underTest.format(notification); + + assertThat(emailMessage.getSubject()).isEqualTo(projectWithBranch.getName() + " (" + projectWithBranch.getBranchName() + "): Background task in failure"); + } + + @Test + public void format_returns_email_with_message_without_branch() { + when(serializer.fromNotification(notification)).thenReturn(new ReportAnalysisFailureNotification( + projectNoBranch, task, errorMessage)); + + EmailMessage emailMessage = underTest.format(notification); + + assertThat(emailMessage.getMessage()) + .contains("Project:\t" + projectNoBranch.getName() + "\n"); + } + + @Test + public void format_returns_email_with_message_with_branch() { + when(serializer.fromNotification(notification)).thenReturn(new ReportAnalysisFailureNotification( + projectWithBranch, task, errorMessage)); + + EmailMessage emailMessage = underTest.format(notification); + + assertThat(emailMessage.getMessage()) + .contains("Project:\t" + projectWithBranch.getName() + " (" + projectWithBranch.getBranchName() + ")\n"); + } + + @Test + public void format_returns_email_with_message_containing_all_information() { + when(serializer.fromNotification(notification)).thenReturn(new ReportAnalysisFailureNotification( + projectNoBranch, task, errorMessage)); + + EmailMessage emailMessage = underTest.format(notification); + + assertThat(emailMessage.getMessage()) + .isEqualTo("Project:\t" + projectNoBranch.getName() + "\n" + + "Background task:\t" + task.getUuid() + "\n" + + "Submission time:\t" + DateUtils.formatDateTime(task.getCreatedAt()) + "\n" + + "Failure time:\t" + DateUtils.formatDateTime(task.getFailedAt()) + "\n" + + "\n" + + "Error message:\t" + errorMessage + "\n" + + "\n" + + "More details at: " + serverUrl + "/project/background_tasks?id=" + projectNoBranch.getKey()); + } + + @Test + public void format_returns_email_with_message_with_error_message_when_null() { + when(serializer.fromNotification(notification)).thenReturn(new ReportAnalysisFailureNotification( + projectNoBranch, task, null)); + + EmailMessage emailMessage = underTest.format(notification); + + assertThat(emailMessage.getMessage()) + .doesNotContain("Error message:\t"); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationModuleTest.java b/server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationModuleTest.java new file mode 100644 index 00000000000..fd4a25c6681 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationModuleTest.java @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.notification; + +import org.junit.Test; +import org.sonar.core.platform.ComponentContainer; +import org.sonar.server.notification.NotificationDispatcherMetadata; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ReportAnalysisFailureNotificationModuleTest { + private ReportAnalysisFailureNotificationModule underTest = new ReportAnalysisFailureNotificationModule(); + + @Test + public void adds_dispatcher_and_its_metadata() { + ComponentContainer container = new ComponentContainer(); + + underTest.configure(container); + + assertThat(container.getPicoContainer().getComponentAdapters(NotificationDispatcherMetadata.class)).isNotNull(); + assertThat(container.getPicoContainer().getComponentAdapters(ReportAnalysisFailureNotificationDispatcher.class)).isNotNull(); + } + + @Test + public void adds_template_and_serializer() { + ComponentContainer container = new ComponentContainer(); + + underTest.configure(container); + + assertThat(container.getPicoContainer().getComponentAdapters(ReportAnalysisFailureNotificationEmailTemplate.class)).isNotNull(); + assertThat(container.getPicoContainer().getComponentAdapters(ReportAnalysisFailureNotificationSerializer.class)).isNotNull(); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationSerializerImplTest.java b/server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationSerializerImplTest.java new file mode 100644 index 00000000000..c1c1ee8d9a4 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationSerializerImplTest.java @@ -0,0 +1,82 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.notification; + +import java.util.Random; +import org.junit.Test; +import org.sonar.api.notifications.Notification; + +import static java.lang.String.valueOf; +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.assertj.core.api.Assertions.assertThat; + +public class ReportAnalysisFailureNotificationSerializerImplTest { + private Random random = new Random(); + private ReportAnalysisFailureNotification.Project project = new ReportAnalysisFailureNotification.Project( + randomAlphanumeric(2), + randomAlphanumeric(3), + randomAlphanumeric(4), + randomAlphanumeric(5)); + private ReportAnalysisFailureNotification.Task task = new ReportAnalysisFailureNotification.Task( + randomAlphanumeric(6), + random.nextInt(99), + random.nextInt(99)); + private String errorMessage = randomAlphanumeric(7); + private ReportAnalysisFailureNotificationSerializerImpl underTest = new ReportAnalysisFailureNotificationSerializerImpl(); + + @Test + public void verify_toNotification() { + + Notification notification = underTest.toNotification(new ReportAnalysisFailureNotification(project, task, errorMessage)); + + assertThat(notification.getFieldValue("project.uuid")).isEqualTo(project.getUuid()); + assertThat(notification.getFieldValue("project.name")).isEqualTo(project.getName()); + assertThat(notification.getFieldValue("project.key")).isEqualTo(project.getKey()); + assertThat(notification.getFieldValue("project.branchName")).isEqualTo(project.getBranchName()); + assertThat(notification.getFieldValue("task.uuid")).isEqualTo(task.getUuid()); + assertThat(notification.getFieldValue("task.createdAt")).isEqualTo(valueOf(task.getCreatedAt())); + assertThat(notification.getFieldValue("task.failedAt")).isEqualTo(valueOf(task.getFailedAt())); + assertThat(notification.getFieldValue("error.message")).isEqualTo(errorMessage); + } + + @Test + public void verify_fromNotification() { + Notification notification = new Notification(randomAlphanumeric(1)) + .setFieldValue("project.uuid", project.getUuid()) + .setFieldValue("project.name", project.getName()) + .setFieldValue("project.key", project.getKey()) + .setFieldValue("project.branchName", project.getBranchName()) + .setFieldValue("task.uuid", task.getUuid()) + .setFieldValue("task.createdAt", valueOf(task.getCreatedAt())) + .setFieldValue("task.failedAt", valueOf(task.getFailedAt())) + .setFieldValue("error.message", errorMessage); + + ReportAnalysisFailureNotification reportAnalysisFailureNotification = underTest.fromNotification(notification); + + assertThat(reportAnalysisFailureNotification.getProject().getUuid()).isEqualTo(project.getUuid()); + assertThat(reportAnalysisFailureNotification.getProject().getKey()).isEqualTo(project.getKey()); + assertThat(reportAnalysisFailureNotification.getProject().getName()).isEqualTo(project.getName()); + assertThat(reportAnalysisFailureNotification.getProject().getBranchName()).isEqualTo(project.getBranchName()); + assertThat(reportAnalysisFailureNotification.getTask().getUuid()).isEqualTo(task.getUuid()); + assertThat(reportAnalysisFailureNotification.getTask().getCreatedAt()).isEqualTo(task.getCreatedAt()); + assertThat(reportAnalysisFailureNotification.getTask().getFailedAt()).isEqualTo(task.getFailedAt()); + assertThat(reportAnalysisFailureNotification.getErrorMessage()).isEqualTo(errorMessage); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationTest.java b/server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationTest.java new file mode 100644 index 00000000000..f23e5d6f7cf --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/ce/notification/ReportAnalysisFailureNotificationTest.java @@ -0,0 +1,138 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.notification; + +import java.util.Random; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.assertj.core.api.Assertions.assertThat; + +public class ReportAnalysisFailureNotificationTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private Random random = new Random(); + private ReportAnalysisFailureNotification.Task task = new ReportAnalysisFailureNotification.Task( + randomAlphanumeric(2), random.nextInt(5_996), random.nextInt(9_635)); + private ReportAnalysisFailureNotification.Project project = new ReportAnalysisFailureNotification.Project( + randomAlphanumeric(6), randomAlphanumeric(7), randomAlphanumeric(8), randomAlphanumeric(9)); + + @Test + public void project_constructor_fails_if_uuid_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("uuid can't be null"); + + new ReportAnalysisFailureNotification.Project(null, randomAlphanumeric(2), randomAlphanumeric(3), randomAlphanumeric(4)); + } + + @Test + public void project_constructor_fails_if_key_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("key can't be null"); + + new ReportAnalysisFailureNotification.Project(randomAlphanumeric(2), null, randomAlphanumeric(3), randomAlphanumeric(4)); + } + + @Test + public void project_constructor_fails_if_name_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("name can't be null"); + + new ReportAnalysisFailureNotification.Project(randomAlphanumeric(2), randomAlphanumeric(3), null, randomAlphanumeric(4)); + } + + @Test + public void verify_report_getters() { + String uuid = randomAlphanumeric(2); + String key = randomAlphanumeric(3); + String name = randomAlphanumeric(4); + String branchName = randomAlphanumeric(5); + ReportAnalysisFailureNotification.Project underTest = new ReportAnalysisFailureNotification.Project(uuid, key, name, branchName); + + assertThat(underTest.getUuid()).isEqualTo(uuid); + assertThat(underTest.getName()).isEqualTo(name); + assertThat(underTest.getKey()).isEqualTo(key); + assertThat(underTest.getBranchName()).isEqualTo(branchName); + } + + @Test + public void project_supports_null_branch() { + ReportAnalysisFailureNotification.Project underTest = new ReportAnalysisFailureNotification.Project(randomAlphanumeric(2), randomAlphanumeric(3), randomAlphanumeric(4), null); + + assertThat(underTest.getBranchName()).isNull(); + } + + @Test + public void task_constructor_fails_if_uuid_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("uuid can't be null"); + + new ReportAnalysisFailureNotification.Task(null, random.nextInt(5_996), random.nextInt(9_635)); + } + + @Test + public void verify_task_getters() { + String uuid = randomAlphanumeric(6); + int createdAt = random.nextInt(5_996); + int failedAt = random.nextInt(9_635); + + ReportAnalysisFailureNotification.Task underTest = new ReportAnalysisFailureNotification.Task(uuid, createdAt, failedAt); + + assertThat(underTest.getUuid()).isEqualTo(uuid); + assertThat(underTest.getCreatedAt()).isEqualTo(createdAt); + assertThat(underTest.getFailedAt()).isEqualTo(failedAt); + } + + @Test + public void constructor_fails_with_NPE_if_Project_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("project can't be null"); + + new ReportAnalysisFailureNotification(null, task, randomAlphanumeric(99)); + } + + @Test + public void constructor_fails_with_NPE_if_Task_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("task can't be null"); + + new ReportAnalysisFailureNotification(project, null, randomAlphanumeric(99)); + } + + @Test + public void verify_getters() { + String message = randomAlphanumeric(99); + ReportAnalysisFailureNotification underTest = new ReportAnalysisFailureNotification(project, task, message); + + assertThat(underTest.getProject()).isSameAs(project); + assertThat(underTest.getTask()).isSameAs(task); + assertThat(underTest.getErrorMessage()).isSameAs(message); + } + + @Test + public void null_error_message_is_supported() { + ReportAnalysisFailureNotification underTest = new ReportAnalysisFailureNotification(project, task, null); + + assertThat(underTest.getErrorMessage()).isNull(); + } +} diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index e16a23b24fd..469ebaa4fe6 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1862,6 +1862,7 @@ notification.dispatcher.NewIssues=New issues notification.dispatcher.NewAlerts=New quality gate status notification.dispatcher.NewFalsePositiveIssue=Issues resolved as false positive or won't fix notification.dispatcher.SQ-MyNewIssues=My new issues +notification.dispatcher.CeReportTaskFailure=Report processing failure #------------------------------------------------------------------------------