All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.kiwiproject.test.curator.CuratorTestingServerExtension Maven / Gradle / Ivy

package org.kiwiproject.test.curator;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.imps.CuratorFrameworkState;
import org.apache.curator.retry.RetryOneTime;
import org.apache.curator.test.TestingServer;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.kiwiproject.net.LocalPortChecker;

import java.io.File;
import java.util.concurrent.TimeUnit;

/**
 * A JUnit Jupiter {@link org.junit.jupiter.api.extension.Extension Extension} that sets up a Curator {@link TestingServer}
 * for (integration) tests that require a ZooKeeper server to be running.
 * 

* This extension starts a {@link TestingServer} before any tests run, and shuts it down after all tests have run. * It should be declared as {@code static} and registered using * {@link org.junit.jupiter.api.extension.RegisterExtension RegisterExtension}. Example: *

 * {@literal @}RegisterExtension
 *  static final CuratorTestingServerExtension ZK_TEST_SERVER = new CuratorTestingServerExtension();
 * 
* The above finds the first open port above 1024 and uses that port for the testing server. You can also * specify a port to start the testing server on, but if that port is not open an exception will be thrown. *

* One important thing to note is that because this uses {@link BeforeAllCallback}, any tests that use * {@link org.junit.jupiter.api.Nested Nested} will cause the {@link BeforeAllCallback#beforeAll(ExtensionContext)} * and {@link AfterAllCallback#afterAll(ExtensionContext)} lifecycle methods to be executed for each nested test * class. This is why the client state checks are the first thing in the beforeAll method. By skipping initialization * if the client is in the STARTED state, we can accommodate using nested classes. We also need to handle when the * state is STOPPED, which will be the case if there is more than one nested test class, and in that case we must * re-initialize the testing server and CuratorFramework. I am not sure if this is the "correct" way to do this in * Jupiter, but it works and doesn't seem like it will hurt anything. *

* If you need the Curator {@link TestingServer} to start immediately instead of waiting for the normal * JUnit Jupiter lifecycle, you can use one of the {@link #newStartingImmediately()} factory methods. You might need * to start immediately in situations where there are other components (e.g. other extensions or code in a test) * that relies on a ZooKeeper server to be running when instantiated. When starting immediately, this extension * should be the first one registered, so that it starts the testing server before other code attempts to connect to * it. Also note we consider starting the testing server immediately to be the exception, not the general case, and * should generally be considered as a last resort. */ @Slf4j public class CuratorTestingServerExtension implements BeforeAllCallback, AfterAllCallback { private static final LocalPortChecker PORT_CHECKER = new LocalPortChecker(); private static final int SESSION_TIMEOUT_MS = 5_000; private static final int CONNECTION_TIMEOUT_MS = 1_000; private static final int SLEEP_BETWEEN_RETRY_MS = 500; private static final int MAX_CONNECT_WAIT_TIME_SECONDS = 5; private TestingServer testingServer; @Getter private CuratorFramework client; /** * Create a new extension that uses the first open port above 1024 that it finds. */ public CuratorTestingServerExtension() { this(findOpenPortOrThrow()); } /** * Create a new extension that uses the given port. * * @param port the port to use */ public CuratorTestingServerExtension(int port) { this(port, false); } /** * Create a new extension that uses the first open port above 1024 that it finds, and which starts the Curator * {@link TestingServer} immediately (before the normal JUnit Jupiter lifecycle). * * @return a new extension instance * @see #newStartingImmediately(int) */ public static CuratorTestingServerExtension newStartingImmediately() { var port = findOpenPortOrThrow(); return newStartingImmediately(port); } /** * Create a new extension using the given port and specifying whether to start the {@link TestingServer} * immediately (before the normal JUnit Jupiter lifecycle). * * @param port the port to use * @return a new extension instance */ public static CuratorTestingServerExtension newStartingImmediately(int port) { return new CuratorTestingServerExtension(port, true); } /** * Create a new extension using the given port and specifying whether to start the {@link TestingServer} * immediately. * * @param port the port to use * @param shouldStartImmediately use {@code true} to start the testing server immediately, or {@code false} to start * during the normal JUnit Jupiter lifecycle (which will be in the * {@link org.junit.jupiter.api.BeforeAll} callback method) */ private CuratorTestingServerExtension(int port, boolean shouldStartImmediately) { initialize(port, shouldStartImmediately); } @Override public void beforeAll(ExtensionContext context) throws Exception { var displayName = context.getDisplayName(); LOG.trace("[beforeAll: {}] Initialize testing server.", displayName); if (client.getState() == CuratorFrameworkState.STARTED) { LOG.trace("[beforeAll: {}] Skip initialization since client is STARTED. Maybe we are in a @Nested test class?", displayName); return; } else if (client.getState() == CuratorFrameworkState.STOPPED) { LOG.trace("[beforeAll: {}] Re-initialize since client is STOPPED. There is probably more than one @Nested test class.", displayName); var newPort = findOpenPortOrThrow(); initialize(newPort, false); } LOG.trace("[beforeAll: {}] Starting TestingServer and CuratorFramework client", displayName); testingServer.start(); client.start(); client.blockUntilConnected(MAX_CONNECT_WAIT_TIME_SECONDS, TimeUnit.SECONDS); LOG.trace("[beforeAll: {}] Testing server with connect string {} started on port {} with temp directory {}; client is connected.", displayName, testingServer.getConnectString(), testingServer.getPort(), testingServer.getTempDirectory()); } private void initialize(int port, boolean startServer) { checkArgument(port >= 0 && port <= 0xFFFF, "Invalid port: %s", port); LOG.trace("Using {} as testing server port. Starting server now? {}", port, startServer); try { checkPortIsAvailable(port); testingServer = new TestingServer(port, startServer); } catch (Exception e) { throw new CuratorTestingServerException("Error creating testing server on port " + port, e); } client = CuratorFrameworkFactory.newClient( testingServer.getConnectString(), SESSION_TIMEOUT_MS, CONNECTION_TIMEOUT_MS, new RetryOneTime(SLEEP_BETWEEN_RETRY_MS) ); } private void checkPortIsAvailable(int port) { checkState(new LocalPortChecker().isPortAvailable(port), "port %s is not available", port); } private static int findOpenPortOrThrow() { return PORT_CHECKER.findFirstOpenPortAbove(1024).orElseThrow(IllegalStateException::new); } @Override public void afterAll(ExtensionContext context) { var displayName = context.getDisplayName(); try { LOG.trace("[afterAll: {}] Closing client and test server", displayName); client.close(); testingServer.close(); } catch (Exception e) { throw new CuratorTestingServerException("Error stopping testing server on port " + getPort(), e); } LOG.trace("[afterAll: {}] Client and test server closed", displayName); } /** * @return the connection string of the {@link TestingServer} */ public String getConnectString() { return testingServer.getConnectString(); } /** * @return the port of the {@link TestingServer} */ public int getPort() { return testingServer.getPort(); } /** * @return the temporary directory that the {@link TestingServer} is using */ public File getTempDirectory() { return testingServer.getTempDirectory(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy