/* * Copyright 2014 GoDataDriven B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.divolte.server; import com.google.common.base.Preconditions; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import com.typesafe.config.ConfigValueFactory; import io.divolte.server.config.ValidatedConfiguration; import org.apache.avro.generic.GenericRecord; import javax.annotation.ParametersAreNonnullByDefault; import java.io.IOException; import java.io.UncheckedIOException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.UnknownHostException; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; public final class ServerTestUtils { @SuppressWarnings("PMD.AvoidUsingHardCodedIP") private static final String LOOPBACK = "127.0.0.1"; /* * List of ports to cycle through. * * We use this list instead of a random OS-assigned port because * some environments (e.g. Sauce Labs) only support specific ports * when connecting to 'localhost'. * See: https://wiki.saucelabs.com/display/DOCS/Sauce+Connect+Proxy+FAQS#SauceConnectProxyFAQS-CanIAccessApplicationsonlocalhost? * * Note: Browsers often block ports on localhost. * https://fetch.spec.whatwg.org/#port-blocking * (Safari and Firefox use this list; not sure about the rest.) * Note: Android can't use 5555 or 8080 with Sauce Connect. */ private static int[] SAFE_PORTS = { 2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, 3030, 3210, 3333, 4000, 4001, 4040, 4321, 4502, 4503, 4567, 5000, 5001, 5050, 5432, 6001, 6060, 6543, 7000, 7070, 7774, 7777, 8000, 8001, 8003, 8031, 8081, 8765, 8777, 8888, 9000, 9001, 9080, 9090, 9876, 9877, 9999, 49221, 55001, }; private static AtomicInteger nextSafePortIndex = new AtomicInteger(0); @SuppressWarnings("PMD.EmptyCatchBlock") private static int findFreePort() { for(int attempt = 0; attempt < SAFE_PORTS.length * 2; ++attempt) { final int candidatePortIndex = nextSafePortIndex.getAndUpdate((lastPort) -> (lastPort + 1) % SAFE_PORTS.length); try (final ServerSocket socket = new ServerSocket(SAFE_PORTS[candidatePortIndex])) { return socket.getLocalPort(); } catch (final IOException e) { // Assume port already in use. Proceed to next one... } } // Give up if we go through the list twice. throw new RuntimeException("Could not find unused safe port."); } private static Config REFERENCE_TEST_CONFIG = ConfigFactory.parseResources("reference-test.conf"); @ParametersAreNonnullByDefault public static final class EventPayload { final DivolteEvent event; final AvroRecordBuffer buffer; final GenericRecord record; public EventPayload(final DivolteEvent event, final AvroRecordBuffer buffer, final GenericRecord record) { this.event = Objects.requireNonNull(event); this.buffer = Objects.requireNonNull(buffer); this.record = Objects.requireNonNull(record); } } @ParametersAreNonnullByDefault public static final class TestServer { final Config config; final String host; final int port; private final Server server; final BlockingQueue<EventPayload> events; public TestServer() { this(findFreePort(), REFERENCE_TEST_CONFIG); } public TestServer(final String configResource) { this(findFreePort(), ConfigFactory.parseResources(configResource) .withFallback(REFERENCE_TEST_CONFIG)); } public TestServer(final String configResource, final Map<String,Object> extraConfig) { this(findFreePort(), ConfigFactory.parseMap(extraConfig, "Test-specific overrides") .withFallback(ConfigFactory.parseResources(configResource)) .withFallback(REFERENCE_TEST_CONFIG)); } private TestServer(final int port, final Config config) { this.port = port; this.host = getBindAddress(); this.config = config.withValue("divolte.global.server.host", ConfigValueFactory.fromAnyRef(host)) .withValue("divolte.global.server.port", ConfigValueFactory.fromAnyRef(port)); events = new ArrayBlockingQueue<>(100); final ValidatedConfiguration vc = new ValidatedConfiguration(() -> this.config); Preconditions.checkArgument(vc.isValid(), "Invalid test server configuration: %s", vc.errors()); server = new Server(vc, (event, buffer, record) -> events.add(new EventPayload(event, buffer, record))); server.run(); } private static String getBindAddress() { final String bindAddress; if (Boolean.getBoolean("io.divolte.test.bindExternal")) { try { bindAddress = InetAddress.getLocalHost().getHostAddress(); } catch (final UnknownHostException e) { throw new UncheckedIOException("Unable to determine external IP address", e); } } else { bindAddress = LOOPBACK; } return bindAddress; } static TestServer createTestServerWithDefaultNonTestConfiguration() { return new TestServer(findFreePort(), ConfigFactory.defaultReference()); } public EventPayload waitForEvent() throws InterruptedException { // SauceLabs can take quite a while to fire up everything. return waitForEvent(10, TimeUnit.SECONDS); } public EventPayload waitForEvent(final long timeout, final TimeUnit unit) throws InterruptedException { return Optional.ofNullable(events.poll(timeout, unit)) .orElseThrow(() -> new RuntimeException("Timed out while waiting for server side event to occur.")); } public boolean eventsRemaining() { return !events.isEmpty(); } public void shutdown() { shutdown(false); } public void shutdown(final boolean waitForShutdown) { // The server can take a little while to shut down, so we do this asynchronously if possible. // (This is harmless: new servers will listen on a different port.) if (waitForShutdown) { server.shutdown(); } else { new Thread(server::shutdown).start(); } } } }