/******************************************************************************* * Copyright French Prime minister Office/SGMAP/DINSIC/Vitam Program (2015-2019) * * contact.vitam@culture.gouv.fr * * This software is a computer program whose purpose is to implement a digital archiving back-office system managing * high volumetry securely and efficiently. * * This software is governed by the CeCILL 2.1 license under French law and abiding by the rules of distribution of free * software. You can use, modify and/ or redistribute the software under the terms of the CeCILL 2.1 license as * circulated by CEA, CNRS and INRIA at the following URL "http://www.cecill.info". * * As a counterpart to the access to the source code and rights to copy, modify and redistribute granted by the license, * users are provided only with a limited warranty and the software's author, the holder of the economic rights, and the * successive licensors have only limited liability. * * In this respect, the user's attention is drawn to the risks associated with loading, using, modifying and/or * developing or reproducing the software by the user in light of its specific status of free software, that may mean * that it is complicated to manipulate, and that also therefore means that it is reserved for developers and * experienced professionals having in-depth computer knowledge. Users are therefore encouraged to load and test the * software's suitability as regards their requirements in conditions enabling the security of their systems and/or data * to be ensured and, more generally, to use and operate it in the same conditions as regards security. * * The fact that you are presently reading this means that you have had knowledge of the CeCILL 2.1 license and that you * accept its terms. *******************************************************************************/ package fr.gouv.vitam.common.junit; import static org.elasticsearch.node.NodeBuilder.nodeBuilder; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.net.ServerSocket; import java.net.Socket; import java.util.HashSet; import java.util.Set; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.node.Node; import org.elasticsearch.transport.BindTransportException; import org.junit.rules.ExternalResource; import org.junit.rules.TemporaryFolder; import com.google.common.testing.GcFinalization; import fr.gouv.vitam.common.SystemPropertyUtil; import fr.gouv.vitam.common.VitamConfiguration; import fr.gouv.vitam.common.exception.VitamApplicationServerException; import fr.gouv.vitam.common.junit.VitamApplicationTestFactory.StartApplicationResponse; import fr.gouv.vitam.common.logging.SysErrLogger; import fr.gouv.vitam.common.logging.VitamLogger; import fr.gouv.vitam.common.logging.VitamLoggerFactory; /** * This class allows to get an available port during Junit execution */ public class JunitHelper extends ExternalResource { private static final int WAIT_BETWEEN_TRY = 10; private static final int WAIT_AFTER_FULL_GC = 100; private static final int MAX_PORT = 65535; private static final int BUFFER_SIZE = 65536; private static final String COULD_NOT_FIND_A_FREE_TCP_IP_PORT_TO_START_EMBEDDED_SERVER_ON = "Could not find a free TCP/IP port to start embedded Server on"; private static final VitamLogger LOGGER = VitamLoggerFactory.getInstance(JunitHelper.class); private final Set<Integer> portAlreadyUsed = new HashSet<>(); /** * Jetty port SystemProperty */ private static final String PARAMETER_JETTY_SERVER_PORT = "jetty.port"; private static final JunitHelper JUNIT_HELPER = new JunitHelper(); /** * Empty constructor */ private JunitHelper() { // Empty } /** * * @return the unique instance */ public static final JunitHelper getInstance() { return JUNIT_HELPER; } /** * @return an available port if it exists * @throws IllegalStateException if no port available */ public final synchronized int findAvailablePort() { return getAvailablePort(); } /** * Find an available port, set the Property jetty.port and call the factory to start the application in one * synchronized step. * * @param testFactory the {@link VitamApplicationTestFactory} to use * @return the available and used port if it exists and the started application * @throws IllegalStateException if no port available */ public final synchronized StartApplicationResponse<?> findAvailablePortSetToApplication( VitamApplicationTestFactory<?> testFactory) { if (testFactory == null) { throw new IllegalStateException("Factory must not be null"); } final int port = getAvailablePort(); try { final StartApplicationResponse<?> response = testFactory.startVitamApplication(port); final int realPort = response.getServerPort(); if (realPort <= 0) { portAlreadyUsed.remove(Integer.valueOf(port)); throw new IllegalStateException(COULD_NOT_FIND_A_FREE_TCP_IP_PORT_TO_START_EMBEDDED_SERVER_ON); } if (realPort != port) { portAlreadyUsed.add(Integer.valueOf(realPort)); portAlreadyUsed.remove(Integer.valueOf(port)); } return response; } finally { unsetJettyPortSystemProperty(); } } private final int getAvailablePort() { do { final Integer port = getPort(); if (!portAlreadyUsed.contains(port)) { portAlreadyUsed.add(port); LOGGER.debug("Available port: " + port); setJettyPortSystemProperty(port); return port.intValue(); } try { Thread.sleep(WAIT_BETWEEN_TRY); } catch (final InterruptedException e) { SysErrLogger.FAKE_LOGGER.ignoreLog(e); } } while (true); } /** * Remove the used port * * @param port */ public final synchronized void releasePort(int port) { LOGGER.debug("Relaese port: " + port); portAlreadyUsed.remove(Integer.valueOf(port)); unsetJettyPortSystemProperty(); } private final int getPort() { try (ServerSocket socket = new ServerSocket(0)) { socket.setReuseAddress(true); return socket.getLocalPort(); } catch (final IOException e) { LOGGER.error(COULD_NOT_FIND_A_FREE_TCP_IP_PORT_TO_START_EMBEDDED_SERVER_ON, e); throw new IllegalStateException(COULD_NOT_FIND_A_FREE_TCP_IP_PORT_TO_START_EMBEDDED_SERVER_ON, e); } } /** * @param port the port to check on localhost * @return True if the port is used by the localhost server * @throws IllegalArgumentException if the port is not between 1 and 65535 */ public final boolean isListeningOn(int port) { return isListeningOn(null, port); } /** * @param host the host to check * @param port the port to check on host * @return True if the port is used by the specified host * @throws IllegalArgumentException if the port is not between 1 and 65535 */ public final boolean isListeningOn(String host, int port) { if (port < 1 || port > MAX_PORT) { throw new IllegalArgumentException("Port must be between 1 and 65535"); } try (Socket socket = new Socket(host, port)) { return true; } catch (final IOException e) { LOGGER.warn("The server is not listening on specified port", e); return false; } } /** * Read and close the inputStream using buffer read (read(buffer)) * * @param inputStream * @return the size of the inputStream read */ public static final long consumeInputStream(InputStream inputStream) { long read = 0; if (inputStream == null) { return read; } final byte[] buffer = new byte[BUFFER_SIZE]; try { int len = 0; while ((len = inputStream.read(buffer)) >= 0) { read += len; } } catch (final IOException e) { SysErrLogger.FAKE_LOGGER.ignoreLog(e); } try { inputStream.close(); } catch (final IOException e) { SysErrLogger.FAKE_LOGGER.ignoreLog(e); } return read; } /** * Read and close the inputStream one byte at a time (read()) * * @param inputStream * @return the size of the inputStream read */ public static final long consumeInputStreamPerByte(InputStream inputStream) { long read = 0; if (inputStream == null) { return read; } try { while (inputStream.read() >= 0) { read++; } } catch (final IOException e) { LOGGER.debug(e); } try { inputStream.close(); } catch (final IOException e) { SysErrLogger.FAKE_LOGGER.ignoreLog(e); } return read; } /** * For benchmark: clean the used memory using a full GC.</br> * </br> * Usage:</br> * JunitHelper.awaitFullGc();</br> * long firstAvailableMemory = Runtime.getRuntime().freeMemory();</br> * ... do some tests consuming memory JunitHelper.awaitFullGc();</br> * long secondAvailableMemory = Runtime.getRuntime().freeMemory();</br> * long usedMemory = firstAvailableMemory - secondAvailableMemory; */ public static final void awaitFullGc() { GcFinalization.awaitFullGc(); try { Thread.sleep(WAIT_AFTER_FULL_GC); } catch (final InterruptedException e) { SysErrLogger.FAKE_LOGGER.ignoreLog(e); } } /** * Set JettyPort System Property * * @param port */ public static final void setJettyPortSystemProperty(int port) { SystemPropertyUtil.set(PARAMETER_JETTY_SERVER_PORT, Integer.toString(port)); } /** * Unset JettyPort System Property */ public static final void unsetJettyPortSystemProperty() { SystemPropertyUtil.clear(PARAMETER_JETTY_SERVER_PORT); } /** * Utility to check empty private constructor * * @param clasz */ public static final void testPrivateConstructor(Class<?> clasz) { // Get the empty constructor Constructor<?> c; try { c = clasz.getDeclaredConstructor(); // Set it accessible c.setAccessible(true); // finally call the constructor c.newInstance(); } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | UnsupportedOperationException e) { SysErrLogger.FAKE_LOGGER.ignoreLog(e); } } /** * Class to help to build and stop an Elasticsearch server */ public static class ElasticsearchTestConfiguration { int tcpPort; int httpPort; File elasticsearchHome; Node node; /** * * @return the associated TCP PORT */ public int getTcpPort() { return tcpPort; } /** * * @return the associated HTTP PORT */ public int getHttpPort() { return httpPort; } /** * * @return the associated Home */ public File getElasticsearchHome() { return elasticsearchHome; } /** * * @return the associated Node */ public Node getNode() { return node; } } private static final void tryStartElasticsearch(ElasticsearchTestConfiguration config, TemporaryFolder tempFolder, String clusterName) { try { config.elasticsearchHome = tempFolder.newFolder(); final Settings settings = Settings.settingsBuilder() .put("http.enabled", true) .put("discovery.zen.ping.multicast.enabled", false) .put("transport.tcp.port", config.tcpPort) .put("http.port", config.httpPort) .put("path.home", config.elasticsearchHome.getCanonicalPath()) .put("transport.tcp.connect_timeout", "1s") .put("transport.profiles.tcp.connect_timeout", "1s") .put("watcher.http.default_read_timeout", VitamConfiguration.getReadTimeout() / 1000 + "s") .build(); config.node = nodeBuilder() .settings(settings) .client(false) .clusterName(clusterName) .node(); config.node.start(); } catch (BindTransportException | IOException e) { SysErrLogger.FAKE_LOGGER.ignoreLog(e); config.node = null; try { Thread.sleep(WAIT_BETWEEN_TRY); } catch (final InterruptedException e1) { SysErrLogger.FAKE_LOGGER.ignoreLog(e1); } } if (config.node != null) { return; } } /** * * Helper to start an Elasticsearch server * * @param tempFolder the TemporaryFolder declared as ClassRule within the Junit class * @param clusterName the cluster name * @param tcpPort the given TcpPort * @param httpPort the given HttpPort * @return the ElasticsearchTestConfiguration to pass to stopElasticsearchForTest * @throws VitamApplicationServerException if the Elasticsearch server cannot be started */ public static final ElasticsearchTestConfiguration startElasticsearchForTest(TemporaryFolder tempFolder, String clusterName, int tcpPort, int httpPort) throws VitamApplicationServerException { final ElasticsearchTestConfiguration config = new ElasticsearchTestConfiguration(); config.httpPort = httpPort; config.tcpPort = tcpPort; for (int i = 0; i < VitamConfiguration.getRetryNumber(); i++) { tryStartElasticsearch(config, tempFolder, clusterName); if (config.node != null) { return config; } } throw new VitamApplicationServerException("Cannot start Elasticsearch"); } /** * Helper to start an Elasticsearch server * * @param tempFolder the TemporaryFolder declared as ClassRule within the Junit class * @param clusterName the cluster name * @return the ElasticsearchTestConfiguration to pass to stopElasticsearchForTest * @throws VitamApplicationServerException if the Elasticsearch server cannot be started */ public static final ElasticsearchTestConfiguration startElasticsearchForTest(TemporaryFolder tempFolder, String clusterName) throws VitamApplicationServerException { final JunitHelper junitHelper = getInstance(); final ElasticsearchTestConfiguration config = new ElasticsearchTestConfiguration(); for (int i = 0; i < VitamConfiguration.getRetryNumber(); i++) { config.tcpPort = junitHelper.findAvailablePort(); config.httpPort = junitHelper.findAvailablePort(); tryStartElasticsearch(config, tempFolder, clusterName); if (config.node == null) { junitHelper.releasePort(config.tcpPort); junitHelper.releasePort(config.httpPort); config.node = null; } else { return config; } } throw new VitamApplicationServerException("Cannot start Elasticsearch"); } /** * Stop the Elasticsearch server started through start ElasticsearchForTest * * @param config the ElasticsearchTestConfiguration */ public static final void stopElasticsearchForTest(ElasticsearchTestConfiguration config) { if (config != null) { final JunitHelper junitHelper = getInstance(); if (config.node != null) { config.node.close(); } junitHelper.releasePort(config.tcpPort); junitHelper.releasePort(config.httpPort); } } }