/*
* 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.app;
import com.google.common.base.MoreObjects;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.DisableOnDebug;
import org.junit.rules.ExpectedException;
import org.junit.rules.TestRule;
import org.junit.rules.Timeout;
import org.mockito.Mockito;
import org.sonar.ce.ComputeEngine;
import org.sonar.process.MinimumViableSystem;
import org.sonar.process.Monitored;
import static com.google.common.base.Preconditions.checkState;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
public class CeServerTest {
@Rule
public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60));
@Rule
public ExpectedException expectedException = ExpectedException.none();
private CeServer underTest = null;
private Thread waitingThread = null;
private MinimumViableSystem minimumViableSystem = mock(MinimumViableSystem.class, Mockito.RETURNS_MOCKS);
@After
public void tearDown() throws Exception {
if (underTest != null) {
underTest.stop();
}
Thread waitingThread = this.waitingThread;
this.waitingThread = null;
if (waitingThread != null) {
waitingThread.join();
}
}
@Test
public void constructor_does_not_start_a_new_Thread() throws IOException {
int activeCount = Thread.activeCount();
newCeServer();
assertThat(Thread.activeCount()).isSameAs(activeCount);
}
@Test
public void start_starts_a_new_Thread() throws IOException {
int activeCount = Thread.activeCount();
newCeServer().start();
assertThat(Thread.activeCount()).isSameAs(activeCount + 1);
}
@Test
public void start_throws_ISE_when_called_twice() throws IOException {
CeServer ceServer = newCeServer();
ceServer.start();
expectedException.expect(IllegalStateException.class);
expectedException.expectMessage("start() can not be called twice");
ceServer.start();
}
@Test
public void getStatus_throws_ISE_when_called_before_start() throws IOException {
CeServer ceServer = newCeServer();
expectedException.expect(IllegalStateException.class);
expectedException.expectMessage("getStatus() can not be called before start()");
ceServer.getStatus();
}
@Test
public void getStatus_does_not_return_OPERATIONAL_until_ComputeEngine_startup_returns() throws InterruptedException, IOException {
BlockingStartupComputeEngine computeEngine = new BlockingStartupComputeEngine(null);
CeServer ceServer = newCeServer(computeEngine);
ceServer.start();
assertThat(ceServer.getStatus()).isEqualTo(Monitored.Status.DOWN);
// release ComputeEngine startup method
computeEngine.releaseStartup();
while (ceServer.getStatus() == Monitored.Status.DOWN) {
// wait for isReady to change to true, otherwise test will fail with timeout
}
assertThat(ceServer.getStatus()).isEqualTo(Monitored.Status.OPERATIONAL);
}
@Test
public void getStatus_returns_OPERATIONAL_when_ComputeEngine_startup_throws_any_Exception_or_Error() throws InterruptedException, IOException {
Throwable startupException = new Throwable("Faking failing ComputeEngine#startup()");
BlockingStartupComputeEngine computeEngine = new BlockingStartupComputeEngine(startupException);
CeServer ceServer = newCeServer(computeEngine);
ceServer.start();
assertThat(ceServer.getStatus()).isEqualTo(Monitored.Status.DOWN);
// release ComputeEngine startup method which will throw startupException
computeEngine.releaseStartup();
while (ceServer.getStatus() == Monitored.Status.DOWN) {
// wait for isReady to change to not DOWN, otherwise test will fail with timeout
}
assertThat(ceServer.getStatus()).isEqualTo(Monitored.Status.OPERATIONAL);
}
@Test
public void awaitStop_throws_ISE_if_called_before_start() throws IOException {
CeServer ceServer = newCeServer();
expectedException.expect(IllegalStateException.class);
expectedException.expectMessage("awaitStop() must not be called before start()");
ceServer.awaitStop();
}
@Test
public void awaitStop_throws_ISE_if_called_twice() throws InterruptedException, IOException {
final CeServer ceServer = newCeServer();
ExceptionCatcherWaitingThread waitingThread1 = new ExceptionCatcherWaitingThread(ceServer);
ExceptionCatcherWaitingThread waitingThread2 = new ExceptionCatcherWaitingThread(ceServer);
ceServer.start();
waitingThread1.start();
waitingThread2.start();
while (waitingThread1.isAlive() && waitingThread2.isAlive()) {
// wait for either thread to stop because ceServer.awaitStop() failed with an exception
// if none stops, the test will fail with timeout
}
Exception exception = MoreObjects.firstNonNull(waitingThread1.getException(), waitingThread2.getException());
assertThat(exception)
.isInstanceOf(IllegalStateException.class)
.hasMessage("There can't be more than one thread waiting for the Compute Engine to stop");
assertThat(waitingThread1.getException() != null && waitingThread2.getException() != null).isFalse();
}
@Test
public void awaitStop_keeps_blocking_calling_thread_even_if_calling_thread_is_interrupted_but_until_stop_is_called() throws Exception {
final CeServer ceServer = newCeServer();
Thread waitingThread = newWaitingThread(ceServer::awaitStop);
ceServer.start();
waitingThread.start();
// interrupts waitingThread 5 times in a row (we really insist)
for (int i = 0; i < 5; i++) {
waitingThread.interrupt();
Thread.sleep(5);
assertThat(waitingThread.isAlive()).isTrue();
}
ceServer.stop();
// wait for waiting thread to stop because we stopped ceServer
// if it does not, the test will fail with timeout
waitingThread.join();
}
@Test
public void awaitStop_unblocks_when_waiting_for_ComputeEngine_startup_fails() throws InterruptedException, IOException {
CeServer ceServer = newCeServer(new ComputeEngine() {
@Override
public void startup() {
throw new Error("Faking ComputeEngine.startup() failing");
}
@Override
public void shutdown() {
throw new UnsupportedOperationException("shutdown() should never be called in this context");
}
});
ceServer.start();
// if awaitStop does not unblock, the test will fail with timeout
ceServer.awaitStop();
}
@Test
public void stop_releases_thread_in_awaitStop_even_when_ComputeEngine_shutdown_fails() throws InterruptedException, IOException {
final CeServer ceServer = newCeServer(new ComputeEngine() {
@Override
public void startup() {
// nothing to do at startup
}
@Override
public void shutdown() {
throw new Error("Faking ComputeEngine.shutdown() failing");
}
});
Thread waitingThread = newWaitingThread(ceServer::awaitStop);
ceServer.start();
waitingThread.start();
ceServer.stop();
// wait for waiting thread to stop because we stopped ceServer
// if it does not, the test will fail with timeout
waitingThread.join();
}
private CeServer newCeServer() throws IOException {
return newCeServer(DoNothingComputeEngine.INSTANCE);
}
private CeServer newCeServer(ComputeEngine computeEngine) throws IOException {
checkState(this.underTest == null, "Only one CeServer can be created per test method");
this.underTest = new CeServer(
computeEngine, minimumViableSystem);
return underTest;
}
private Thread newWaitingThread(Runnable runnable) {
Thread t = new Thread(runnable);
checkState(this.waitingThread == null, "Only one waiting thread can be created per test method");
this.waitingThread = t;
return t;
}
private static class BlockingStartupComputeEngine implements ComputeEngine {
private final CountDownLatch latch = new CountDownLatch(1);
@CheckForNull
private final Throwable throwable;
public BlockingStartupComputeEngine(@Nullable Throwable throwable) {
this.throwable = throwable;
}
@Override
public void startup() {
try {
latch.await(1000, MILLISECONDS);
} catch (InterruptedException e) {
throw new RuntimeException("await failed", e);
}
if (throwable != null) {
if (throwable instanceof Error) {
throw (Error) throwable;
} else if (throwable instanceof RuntimeException) {
throw (RuntimeException) throwable;
}
}
}
@Override
public void shutdown() {
// do nothing
}
private void releaseStartup() {
this.latch.countDown();
}
}
private static class ExceptionCatcherWaitingThread extends Thread {
private final CeServer ceServer;
@CheckForNull
private Exception exception = null;
public ExceptionCatcherWaitingThread(CeServer ceServer) {
this.ceServer = ceServer;
}
@Override
public void run() {
try {
ceServer.awaitStop();
} catch (Exception e) {
this.exception = e;
}
}
@CheckForNull
public Exception getException() {
return exception;
}
}
private enum DoNothingComputeEngine implements ComputeEngine {
INSTANCE;
@Override
public void startup() {
// do nothing
}
@Override
public void shutdown() {
// do nothing
}
}
}