import org.sonar.process.ProcessProperties;
import org.sonar.process.Props;
import org.sonar.process.logging.LogbackHelper;
+import org.sonar.server.async.AsyncExecutionModule;
import org.sonar.server.component.ComponentFinder;
import org.sonar.server.component.index.ComponentIndexer;
import org.sonar.server.computation.task.projectanalysis.ProjectAnalysisTaskModule;
import org.sonar.server.util.OkHttpClientProvider;
import org.sonar.server.view.index.ViewIndex;
import org.sonar.server.view.index.ViewIndexer;
+import org.sonar.server.webhook.WebhookModule;
import org.sonarqube.ws.Rules;
import static java.util.Objects.requireNonNull;
ServerIdManager.class,
UriReader.class,
ServerImpl.class,
- DefaultOrganizationProviderImpl.class);
+ DefaultOrganizationProviderImpl.class,
+ AsyncExecutionModule.class);
}
private static void populateLevel4(ComponentContainer container, Props props) {
InternalPropertiesImpl.class,
ProjectConfigurationFactory.class,
+ // webhooks
+ WebhookModule.class,
+
// cleaning
CeCleaningModule.class);
assertThat(picoContainer.getComponentAdapters())
.hasSize(
CONTAINER_ITSELF
- + 76 // level 4
+ + 77 // level 4
+ 6 // content of CeConfigurationModule
+ 4 // content of CeQueueModule
+ 4 // content of CeHttpModule
+ 7 // content of CeTaskProcessorModule
+ 4 // content of ReportAnalysisFailureNotificationModule
+ 3 // CeCleaningModule + its content
+ + 4 // WebhookModule
+ 1 // CeDistributedInformation
);
assertThat(picoContainer.getParent().getComponentAdapters()).hasSize(
CONTAINER_ITSELF
- + 5 // level 3
+ + 6 // level 3
+ + 2 // AsyncExecutionModule
);
assertThat(picoContainer.getParent().getParent().getComponentAdapters()).hasSize(
CONTAINER_ITSELF
- + 13 // MigrationConfigurationModule
+ 17 // level 2
+ + 13 // MigrationConfigurationModule
);
assertThat(picoContainer.getParent().getParent().getParent().getComponentAdapters()).hasSize(
COMPONENTS_IN_LEVEL_1_AT_CONSTRUCTION
--- /dev/null
+/*
+ * 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.server.async;
+
+public interface AsyncExecution {
+ /**
+ * Add the specified {@link Runnable} in queue for asynchronous processing.
+ *
+ * This method returns instantly and {@code r} is executed in another thread.
+ *
+ * @throws NullPointerException if r is {@code null}
+ */
+ void addToQueue(Runnable r);
+}
--- /dev/null
+/*
+ * 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.server.async;
+
+public interface AsyncExecutionExecutorService {
+ void addToQueue(Runnable r);
+}
--- /dev/null
+/*
+ * 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.server.async;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.server.util.AbstractStoppableExecutorService;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+public class AsyncExecutionExecutorServiceImpl
+ extends AbstractStoppableExecutorService<ExecutorService>
+ implements AsyncExecutionExecutorService {
+ private static final Logger LOG = Loggers.get(AsyncExecutionExecutorServiceImpl.class);
+
+ private static final int MIN_THREAD_COUNT = 1;
+ private static final int MAX_THREAD_COUNT = 10;
+ private static final int MAX_QUEUE_SIZE = Integer.MAX_VALUE;
+ private static final long KEEP_ALIVE_TIME_IN_MILLISECONDS = 0L;
+
+ public AsyncExecutionExecutorServiceImpl() {
+ super(
+ new ThreadPoolExecutor(
+ MIN_THREAD_COUNT, MAX_THREAD_COUNT,
+ KEEP_ALIVE_TIME_IN_MILLISECONDS, MILLISECONDS,
+ new LinkedBlockingQueue<>(MAX_QUEUE_SIZE),
+ new ThreadFactoryBuilder()
+ .setDaemon(false)
+ .setNameFormat("SQ_async-%d")
+ .setUncaughtExceptionHandler(((t, e) -> LOG.error("Thread " + t + " failed unexpectedly", e)))
+ .build()));
+ }
+
+ @Override
+ public void addToQueue(Runnable r) {
+ this.submit(r);
+ }
+}
--- /dev/null
+/*
+ * 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.server.async;
+
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+import static java.util.Objects.requireNonNull;
+
+public class AsyncExecutionImpl implements AsyncExecution {
+ private static final Logger LOG = Loggers.get(AsyncExecutionImpl.class);
+ private final AsyncExecutionExecutorService executorService;
+
+ public AsyncExecutionImpl(AsyncExecutionExecutorService executorService) {
+ this.executorService = executorService;
+ }
+
+ @Override
+ public void addToQueue(Runnable r) {
+ requireNonNull(r);
+ executorService.addToQueue(() -> {
+ try {
+ r.run();
+ } catch (Exception e) {
+ LOG.error("Asynchronous task failed", e);
+ }
+ });
+ }
+}
--- /dev/null
+/*
+ * 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.server.async;
+
+import org.sonar.core.platform.Module;
+
+public class AsyncExecutionModule extends Module {
+ @Override
+ protected void configureModule() {
+ add(
+ AsyncExecutionExecutorServiceImpl.class,
+ AsyncExecutionImpl.class);
+ }
+}
--- /dev/null
+/*
+ * 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.server.async;
+
+import javax.annotation.ParametersAreNonnullByDefault;
import org.sonar.server.computation.task.step.ComputationSteps;
import org.sonar.server.computation.taskprocessor.MutableTaskResultHolderImpl;
import org.sonar.server.view.index.ViewIndex;
-import org.sonar.server.webhook.WebhookModule;
public final class ProjectAnalysisTaskContainerPopulator implements ContainerPopulator<TaskContainer> {
private static final ReportAnalysisComponentProvider[] NO_REPORT_ANALYSIS_COMPONENT_PROVIDERS = new ReportAnalysisComponentProvider[0];
SmallChangesetQualityGateSpecialCase.class,
// webhooks
- WebhookModule.class,
WebhookPostTask.class);
}
import org.sonar.api.utils.UriReader;
import org.sonar.core.util.DefaultHttpDownloader;
+import org.sonar.server.async.AsyncExecutionModule;
import org.sonar.server.organization.DefaultOrganizationProviderImpl;
import org.sonar.server.organization.OrganizationFlagsImpl;
import org.sonar.server.platform.ServerIdManager;
UriReader.class,
DefaultHttpDownloader.class,
DefaultOrganizationProviderImpl.class,
- OrganizationFlagsImpl.class);
+ OrganizationFlagsImpl.class,
+ AsyncExecutionModule.class);
}
}
* Abstract implementation of StoppableExecutorService that implements the
* stop() method and delegates all methods to the provided ExecutorService instance.
*/
-public abstract class AbstractStoppableExecutorService<T extends ExecutorService> implements StoppableExecutorService {
- protected final T delegate;
+public abstract class AbstractStoppableExecutorService<D extends ExecutorService> implements StoppableExecutorService {
+ protected final D delegate;
- public AbstractStoppableExecutorService(T delegate) {
+ public AbstractStoppableExecutorService(D delegate) {
this.delegate = delegate;
}
import org.sonar.api.utils.log.Loggers;
import org.sonar.core.config.WebhookProperties;
import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.server.async.AsyncExecution;
import static java.lang.String.format;
import static org.sonar.core.config.WebhookProperties.MAX_WEBHOOKS_PER_TYPE;
private final WebhookCaller caller;
private final WebhookDeliveryStorage deliveryStorage;
+ private final AsyncExecution asyncExecution;
- public WebHooksImpl(WebhookCaller caller, WebhookDeliveryStorage deliveryStorage) {
+ public WebHooksImpl(WebhookCaller caller, WebhookDeliveryStorage deliveryStorage, AsyncExecution asyncExecution) {
this.caller = caller;
this.deliveryStorage = deliveryStorage;
+ this.asyncExecution = asyncExecution;
}
@Override
}
WebhookPayload payload = payloadSupplier.get();
- webhooks.forEach(webhook -> {
+ webhooks.forEach(webhook -> asyncExecution.addToQueue(() -> {
WebhookDelivery delivery = caller.call(webhook, payload);
log(delivery);
deliveryStorage.persist(delivery);
- });
- deliveryStorage.purge(analysis.getProjectUuid());
+ }));
+ asyncExecution.addToQueue(() -> deliveryStorage.purge(analysis.getProjectUuid()));
}
private static void log(WebhookDelivery delivery) {
--- /dev/null
+/*
+ * 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.server.async;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class AsyncExecutionExecutorServiceImplTest {
+ private AsyncExecutionExecutorServiceImpl underTest = new AsyncExecutionExecutorServiceImpl();
+
+ @Test
+ public void submit_executes_runnable_in_another_thread() {
+ try (SlowRunnable slowRunnable = new SlowRunnable()) {
+ underTest.submit(slowRunnable);
+ assertThat(slowRunnable.executed).isFalse();
+ }
+ }
+
+ private static final class SlowRunnable implements Runnable, AutoCloseable {
+ private final CountDownLatch latch = new CountDownLatch(1);
+ private volatile boolean executed = false;
+
+ @Override
+ public void run() {
+ try {
+ latch.await(30, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ // ignore
+ }
+ executed = true;
+ }
+
+ @Override
+ public void close() {
+ latch.countDown();
+ }
+ }
+}
--- /dev/null
+/*
+ * 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.server.async;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class AsyncExecutionImplTest {
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ private AsyncExecutionExecutorService synchronousExecutorService = Runnable::run;
+ private AsyncExecutionImpl underTest = new AsyncExecutionImpl(synchronousExecutorService);
+
+ @Test
+ public void addToQueue_fails_with_NPE_if_Runnable_is_null() {
+ expectedException.expect(NullPointerException.class);
+
+ underTest.addToQueue(null);
+ }
+
+ @Test
+ public void addToQueue_submits_runnable_to_executorService_which_does_not_fail_if_Runnable_argument_throws_exception() {
+ underTest.addToQueue(() -> {
+ throw new RuntimeException("Faking an exception thrown by Runnable argument");
+ });
+
+ assertThat(logTester.logs()).hasSize(1);
+ assertThat(logTester.logs(LoggerLevel.ERROR)).containsOnly("Asynchronous task failed");
+ }
+
+ @Test
+ public void addToQueue_submits_runnable_that_fails_if_Runnable_argument_throws_Error() {
+ Error expected = new Error("Faking an exception thrown by Runnable argument");
+ Runnable runnable = () -> {
+ throw expected;
+ };
+
+ expectedException.expect(Error.class);
+ expectedException.expectMessage(expected.getMessage());
+
+ underTest.addToQueue(runnable);
+ }
+}
--- /dev/null
+/*
+ * 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.server.webhook;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.server.async.AsyncExecution;
+
+import static java.util.Objects.requireNonNull;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+public class AsynchronousWebHooksImplTest {
+ private static final long NOW = 1_500_000_000_000L;
+ private static final String PROJECT_UUID = "P1_UUID";
+
+ private final MapSettings settings = new MapSettings();
+ private final TestWebhookCaller caller = new TestWebhookCaller();
+ private final WebhookDeliveryStorage deliveryStorage = mock(WebhookDeliveryStorage.class);
+ private final WebhookPayload mock = mock(WebhookPayload.class);
+ private final RecordingAsyncExecution asyncExecution = new RecordingAsyncExecution();
+
+ private final WebHooksImpl underTest = new WebHooksImpl(caller, deliveryStorage, asyncExecution);
+
+ @Test
+ public void send_global_webhooks() {
+ settings.setProperty("sonar.webhooks.global", "1,2");
+ settings.setProperty("sonar.webhooks.global.1.name", "First");
+ settings.setProperty("sonar.webhooks.global.1.url", "http://url1");
+ settings.setProperty("sonar.webhooks.global.2.name", "Second");
+ settings.setProperty("sonar.webhooks.global.2.url", "http://url2");
+ caller.enqueueSuccess(NOW, 200, 1_234);
+ caller.enqueueFailure(NOW, new IOException("Fail to connect"));
+
+ underTest.sendProjectAnalysisUpdate(settings.asConfig(), new WebHooks.Analysis(PROJECT_UUID, "1", "#1"), () -> mock);
+
+ assertThat(caller.countSent()).isZero();
+ verifyZeroInteractions(deliveryStorage);
+
+ asyncExecution.executeRecorded();
+
+ assertThat(caller.countSent()).isEqualTo(2);
+ verify(deliveryStorage, times(2)).persist(any(WebhookDelivery.class));
+ verify(deliveryStorage).purge(PROJECT_UUID);
+ }
+
+ private static class RecordingAsyncExecution implements AsyncExecution {
+ private final List<Runnable> runnableList = new ArrayList<>();
+
+ @Override
+ public void addToQueue(Runnable r) {
+ runnableList.add(requireNonNull(r));
+ }
+
+ public void executeRecorded() {
+ ArrayList<Runnable> runnables = new ArrayList<>(runnableList);
+ runnableList.clear();
+ runnables.forEach(Runnable::run);
+ }
+ }
+}
--- /dev/null
+/*
+ * 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.server.webhook;
+
+import java.io.IOException;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.server.async.AsyncExecution;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+public class SynchronousWebHooksImplTest {
+
+ private static final long NOW = 1_500_000_000_000L;
+ private static final String PROJECT_UUID = "P1_UUID";
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ private final MapSettings settings = new MapSettings();
+ private final TestWebhookCaller caller = new TestWebhookCaller();
+ private final WebhookDeliveryStorage deliveryStorage = mock(WebhookDeliveryStorage.class);
+ private final WebhookPayload mock = mock(WebhookPayload.class);
+ private final AsyncExecution synchronousAsyncExecution = Runnable::run;
+ private final WebHooksImpl underTest = new WebHooksImpl(caller, deliveryStorage, synchronousAsyncExecution);
+
+ @Test
+ public void isEnabled_returns_false_if_no_webHoolds() {
+ assertThat(underTest.isEnabled(settings.asConfig())).isFalse();
+ }
+
+ @Test
+ public void isEnabled_returns_true_if_one_valid_global_webhook() {
+ settings.setProperty("sonar.webhooks.global", "1");
+ settings.setProperty("sonar.webhooks.global.1.name", "First");
+ settings.setProperty("sonar.webhooks.global.1.url", "http://url1");
+
+ assertThat(underTest.isEnabled(settings.asConfig())).isTrue();
+ }
+
+ @Test
+ public void isEnabled_returns_false_if_only_one_global_webhook_without_url() {
+ settings.setProperty("sonar.webhooks.global", "1");
+ settings.setProperty("sonar.webhooks.global.1.name", "First");
+
+ assertThat(underTest.isEnabled(settings.asConfig())).isFalse();
+ }
+
+ @Test
+ public void isEnabled_returns_false_if_only_one_global_webhook_without_name() {
+ settings.setProperty("sonar.webhooks.global", "1");
+ settings.setProperty("sonar.webhooks.global.1.url", "http://url1");
+
+ assertThat(underTest.isEnabled(settings.asConfig())).isFalse();
+ }
+
+ @Test
+ public void isEnabled_returns_true_if_one_valid_project_webhook() {
+ settings.setProperty("sonar.webhooks.project", "1");
+ settings.setProperty("sonar.webhooks.project.1.name", "First");
+ settings.setProperty("sonar.webhooks.project.1.url", "http://url1");
+
+ assertThat(underTest.isEnabled(settings.asConfig())).isTrue();
+ }
+
+ @Test
+ public void isEnabled_returns_false_if_only_one_project_webhook_without_url() {
+ settings.setProperty("sonar.webhooks.project", "1");
+ settings.setProperty("sonar.webhooks.project.1.name", "First");
+
+ assertThat(underTest.isEnabled(settings.asConfig())).isFalse();
+ }
+
+ @Test
+ public void isEnabled_returns_false_if_only_one_project_webhook_without_name() {
+ settings.setProperty("sonar.webhooks.project", "1");
+ settings.setProperty("sonar.webhooks.project.1.url", "http://url1");
+
+ assertThat(underTest.isEnabled(settings.asConfig())).isFalse();
+ }
+
+ @Test
+ public void do_nothing_if_no_webhooks() {
+ underTest.sendProjectAnalysisUpdate(settings.asConfig(), new WebHooks.Analysis(PROJECT_UUID, "1", "#1"), () -> mock);
+
+ assertThat(caller.countSent()).isEqualTo(0);
+ assertThat(logTester.logs(LoggerLevel.DEBUG)).isEmpty();
+ verifyZeroInteractions(deliveryStorage);
+ }
+
+ @Test
+ public void send_global_webhooks() {
+ settings.setProperty("sonar.webhooks.global", "1,2");
+ settings.setProperty("sonar.webhooks.global.1.name", "First");
+ settings.setProperty("sonar.webhooks.global.1.url", "http://url1");
+ settings.setProperty("sonar.webhooks.global.2.name", "Second");
+ settings.setProperty("sonar.webhooks.global.2.url", "http://url2");
+ caller.enqueueSuccess(NOW, 200, 1_234);
+ caller.enqueueFailure(NOW, new IOException("Fail to connect"));
+
+ underTest.sendProjectAnalysisUpdate(settings.asConfig(), new WebHooks.Analysis(PROJECT_UUID, "1", "#1"), () -> mock);
+
+ assertThat(caller.countSent()).isEqualTo(2);
+ assertThat(logTester.logs(LoggerLevel.DEBUG)).contains("Sent webhook 'First' | url=http://url1 | time=1234ms | status=200");
+ assertThat(logTester.logs(LoggerLevel.DEBUG)).contains("Failed to send webhook 'Second' | url=http://url2 | message=Fail to connect");
+ verify(deliveryStorage, times(2)).persist(any(WebhookDelivery.class));
+ verify(deliveryStorage).purge(PROJECT_UUID);
+ }
+
+ @Test
+ public void send_project_webhooks() {
+ settings.setProperty("sonar.webhooks.project", "1");
+ settings.setProperty("sonar.webhooks.project.1.name", "First");
+ settings.setProperty("sonar.webhooks.project.1.url", "http://url1");
+ caller.enqueueSuccess(NOW, 200, 1_234);
+
+ underTest.sendProjectAnalysisUpdate(settings.asConfig(), new WebHooks.Analysis(PROJECT_UUID, "1", "#1"), () -> mock);
+
+ assertThat(caller.countSent()).isEqualTo(1);
+ assertThat(logTester.logs(LoggerLevel.DEBUG)).contains("Sent webhook 'First' | url=http://url1 | time=1234ms | status=200");
+ verify(deliveryStorage).persist(any(WebhookDelivery.class));
+ verify(deliveryStorage).purge(PROJECT_UUID);
+ }
+
+ @Test
+ public void process_only_the_10_first_global_webhooks() {
+ testMaxWebhooks("sonar.webhooks.global");
+ }
+
+ @Test
+ public void process_only_the_10_first_project_webhooks() {
+ testMaxWebhooks("sonar.webhooks.project");
+ }
+
+ private void testMaxWebhooks(String property) {
+ IntStream.range(1, 15)
+ .forEach(i -> {
+ settings.setProperty(property + "." + i + ".name", "First");
+ settings.setProperty(property + "." + i + ".url", "http://url");
+ caller.enqueueSuccess(NOW, 200, 1_234);
+ });
+ settings.setProperty(property, IntStream.range(1, 15).mapToObj(String::valueOf).collect(Collectors.joining(",")));
+
+ underTest.sendProjectAnalysisUpdate(settings.asConfig(), new WebHooks.Analysis(PROJECT_UUID, "1", "#1"), () -> mock);
+
+ assertThat(caller.countSent()).isEqualTo(10);
+ assertThat(logTester.logs(LoggerLevel.DEBUG).stream().filter(log -> log.contains("Sent"))).hasSize(10);
+ }
+
+}
+++ /dev/null
-/*
- * 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.server.webhook;
-
-import java.io.IOException;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
-import org.junit.Rule;
-import org.junit.Test;
-import org.sonar.api.config.internal.MapSettings;
-import org.sonar.api.utils.log.LogTester;
-import org.sonar.api.utils.log.LoggerLevel;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
-
-public class WebHooksImplTest {
-
- private static final long NOW = 1_500_000_000_000L;
- private static final String PROJECT_UUID = "P1_UUID";
-
- @Rule
- public LogTester logTester = new LogTester();
-
- private final MapSettings settings = new MapSettings();
- private final TestWebhookCaller caller = new TestWebhookCaller();
- private final WebhookDeliveryStorage deliveryStorage = mock(WebhookDeliveryStorage.class);
- private final WebhookPayload mock = mock(WebhookPayload.class);
- private final WebHooksImpl underTest = new WebHooksImpl(caller, deliveryStorage);
-
- @Test
- public void isEnabled_returns_false_if_no_webHoolds() {
- assertThat(underTest.isEnabled(settings.asConfig())).isFalse();
- }
-
- @Test
- public void isEnabled_returns_true_if_one_valid_global_webhook() {
- settings.setProperty("sonar.webhooks.global", "1");
- settings.setProperty("sonar.webhooks.global.1.name", "First");
- settings.setProperty("sonar.webhooks.global.1.url", "http://url1");
-
- assertThat(underTest.isEnabled(settings.asConfig())).isTrue();
- }
-
- @Test
- public void isEnabled_returns_false_if_only_one_global_webhook_without_url() {
- settings.setProperty("sonar.webhooks.global", "1");
- settings.setProperty("sonar.webhooks.global.1.name", "First");
-
- assertThat(underTest.isEnabled(settings.asConfig())).isFalse();
- }
-
- @Test
- public void isEnabled_returns_false_if_only_one_global_webhook_without_name() {
- settings.setProperty("sonar.webhooks.global", "1");
- settings.setProperty("sonar.webhooks.global.1.url", "http://url1");
-
- assertThat(underTest.isEnabled(settings.asConfig())).isFalse();
- }
-
- @Test
- public void isEnabled_returns_true_if_one_valid_project_webhook() {
- settings.setProperty("sonar.webhooks.project", "1");
- settings.setProperty("sonar.webhooks.project.1.name", "First");
- settings.setProperty("sonar.webhooks.project.1.url", "http://url1");
-
- assertThat(underTest.isEnabled(settings.asConfig())).isTrue();
- }
-
- @Test
- public void isEnabled_returns_false_if_only_one_project_webhook_without_url() {
- settings.setProperty("sonar.webhooks.project", "1");
- settings.setProperty("sonar.webhooks.project.1.name", "First");
-
- assertThat(underTest.isEnabled(settings.asConfig())).isFalse();
- }
-
- @Test
- public void isEnabled_returns_false_if_only_one_project_webhook_without_name() {
- settings.setProperty("sonar.webhooks.project", "1");
- settings.setProperty("sonar.webhooks.project.1.url", "http://url1");
-
- assertThat(underTest.isEnabled(settings.asConfig())).isFalse();
- }
-
- @Test
- public void do_nothing_if_no_webhooks() {
- underTest.sendProjectAnalysisUpdate(settings.asConfig(), new WebHooks.Analysis(PROJECT_UUID, "1", "#1"), () -> mock);
-
- assertThat(caller.countSent()).isEqualTo(0);
- assertThat(logTester.logs(LoggerLevel.DEBUG)).isEmpty();
- verifyZeroInteractions(deliveryStorage);
- }
-
- @Test
- public void send_global_webhooks() {
- settings.setProperty("sonar.webhooks.global", "1,2");
- settings.setProperty("sonar.webhooks.global.1.name", "First");
- settings.setProperty("sonar.webhooks.global.1.url", "http://url1");
- settings.setProperty("sonar.webhooks.global.2.name", "Second");
- settings.setProperty("sonar.webhooks.global.2.url", "http://url2");
- caller.enqueueSuccess(NOW, 200, 1_234);
- caller.enqueueFailure(NOW, new IOException("Fail to connect"));
-
- underTest.sendProjectAnalysisUpdate(settings.asConfig(), new WebHooks.Analysis(PROJECT_UUID, "1", "#1"), () -> mock);
-
- assertThat(caller.countSent()).isEqualTo(2);
- assertThat(logTester.logs(LoggerLevel.DEBUG)).contains("Sent webhook 'First' | url=http://url1 | time=1234ms | status=200");
- assertThat(logTester.logs(LoggerLevel.DEBUG)).contains("Failed to send webhook 'Second' | url=http://url2 | message=Fail to connect");
- verify(deliveryStorage, times(2)).persist(any(WebhookDelivery.class));
- verify(deliveryStorage).purge(PROJECT_UUID);
- }
-
- @Test
- public void send_project_webhooks() {
- settings.setProperty("sonar.webhooks.project", "1");
- settings.setProperty("sonar.webhooks.project.1.name", "First");
- settings.setProperty("sonar.webhooks.project.1.url", "http://url1");
- caller.enqueueSuccess(NOW, 200, 1_234);
-
- underTest.sendProjectAnalysisUpdate(settings.asConfig(), new WebHooks.Analysis(PROJECT_UUID, "1", "#1"), () -> mock);
-
- assertThat(caller.countSent()).isEqualTo(1);
- assertThat(logTester.logs(LoggerLevel.DEBUG)).contains("Sent webhook 'First' | url=http://url1 | time=1234ms | status=200");
- verify(deliveryStorage).persist(any(WebhookDelivery.class));
- verify(deliveryStorage).purge(PROJECT_UUID);
- }
-
- @Test
- public void process_only_the_10_first_global_webhooks() {
- testMaxWebhooks("sonar.webhooks.global");
- }
-
- @Test
- public void process_only_the_10_first_project_webhooks() {
- testMaxWebhooks("sonar.webhooks.project");
- }
-
- private void testMaxWebhooks(String property) {
- IntStream.range(1, 15)
- .forEach(i -> {
- settings.setProperty(property + "." + i + ".name", "First");
- settings.setProperty(property + "." + i + ".url", "http://url");
- caller.enqueueSuccess(NOW, 200, 1_234);
- });
- settings.setProperty(property, IntStream.range(1, 15).mapToObj(String::valueOf).collect(Collectors.joining(",")));
-
- underTest.sendProjectAnalysisUpdate(settings.asConfig(), new WebHooks.Analysis(PROJECT_UUID, "1", "#1"), () -> mock);
-
- assertThat(caller.countSent()).isEqualTo(10);
- assertThat(logTester.logs(LoggerLevel.DEBUG).stream().filter(log -> log.contains("Sent"))).hasSize(10);
- }
-
-}