/* * Copyright 2017 the original author or authors. * * 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 org.gradle.test.fixtures.server.http; import com.sun.net.httpserver.HttpServer; import org.junit.rules.ExternalResource; import java.io.File; import java.io.IOException; import java.net.InetSocketAddress; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * An HTTP server that allows a test to synchronize and make assertions about concurrent activities that happen in another process. * For example, can be used to that certain tasks do or do not execute in parallel. */ public class BlockingHttpServer extends ExternalResource { private static final AtomicInteger COUNTER = new AtomicInteger(); private static final ExecutorService EXECUTOR_SERVICE = Executors.newCachedThreadPool(); private final Lock lock = new ReentrantLock(); private final HttpServer server; private final ChainingHttpHandler handler; private final int timeoutMs; private final int serverId; private boolean running; public BlockingHttpServer() throws IOException { this(30000); } public BlockingHttpServer(int timeoutMs) throws IOException { // Use an OS selected port server = HttpServer.create(new InetSocketAddress(0), 10); server.setExecutor(EXECUTOR_SERVICE); serverId = COUNTER.incrementAndGet(); handler = new ChainingHttpHandler(lock, COUNTER); server.createContext("/", handler); this.timeoutMs = timeoutMs; } /** * Returns the URI for this server. */ public URI getUri() { try { return new URI("http://localhost:" + getPort()); } catch (URISyntaxException e) { throw new RuntimeException(e); } } /** * Returns the URI for the given call. */ public URI uri(String call) { try { return new URI("http", null, "localhost", getPort(), "/" + call, null, null); } catch (URISyntaxException e) { throw new RuntimeException(e); } } /** * Returns Java statements that invoke the given call. */ public String callFromBuild(String call) { URI uri = uri(call); return "System.out.println(\"calling " + uri + "\"); try { new java.net.URL(\"" + uri + "\").openConnection().getContentLength(); } catch(Exception e) { throw new RuntimeException(e); }; System.out.println(\"response received\");"; } /** * Returns a Java statements that invokes a call, using the given expression to calculate the call to make. */ public String callFromBuildUsingExpression(String expression) { String uriExpression = "\"" + getUri() + "/\" + " + expression; return "System.out.println(\"calling \" + " + uriExpression + "); try { new java.net.URL(" + uriExpression + ").openConnection().getContentLength(); } catch(Exception e) { throw new RuntimeException(e); }; System.out.println(\"response received\");"; } /** * Expects the given requests to be made concurrently. Blocks each request until they have all been received then releases them all. */ public void expectConcurrentExecution(String expectedCall, String... additionalExpectedCalls) { List<ResourceHandler> resourceHandlers = new ArrayList<ResourceHandler>(); resourceHandlers.add(new SimpleResourceHandler(expectedCall)); for (String call : additionalExpectedCalls) { resourceHandlers.add(new SimpleResourceHandler(call)); } handler.addHandler(new CyclicBarrierRequestHandler(lock, timeoutMs, resourceHandlers)); } /** * Expects the given requests to be made concurrently. Blocks each request until they have all been received then releases them all. */ public void expectConcurrentExecution(Collection<String> expectedCalls) { List<ResourceHandler> resourceHandlers = new ArrayList<ResourceHandler>(); for (String call : expectedCalls) { resourceHandlers.add(new SimpleResourceHandler(call)); } handler.addHandler(new CyclicBarrierRequestHandler(lock, timeoutMs, resourceHandlers)); } /** * Expects the given requests to be made concurrently. Blocks each request until they have all been received then releases them all. */ public void expectConcurrentExecutionTo(Collection<? extends Resource> expectedCalls) { List<ResourceHandler> resourceHandlers = new ArrayList<ResourceHandler>(); for (Resource call : expectedCalls) { resourceHandlers.add((ResourceHandler) call); } handler.addHandler(new CyclicBarrierRequestHandler(lock, timeoutMs, resourceHandlers)); } /** * Expect a GET request to the given path, and return the contents of the given file. */ public Resource file(String path, File file) { return new FileResourceHandler(path, file); } /** * Expect a GET request to the given path, and return some arbitrary content. */ public Resource resource(String path) { return new SimpleResourceHandler(path); } /** * Expect a GET request to the given path, and return the given content (UTF-8 encoded) */ public Resource resource(String path, String content) { return new SimpleResourceHandler(path, content); } /** * Expects exactly the given number of calls to be made concurrently from any combination of the expected calls. Blocks each call until they are explicitly released. * Is not considered "complete" until all expected calls have been received. */ public BlockingHandler blockOnConcurrentExecutionAnyOf(int concurrent, String... expectedCalls) { List<ResourceHandler> resourceHandlers = new ArrayList<ResourceHandler>(); for (String call : expectedCalls) { resourceHandlers.add(new SimpleResourceHandler(call)); } CyclicBarrierAnyOfRequestHandler requestHandler = new CyclicBarrierAnyOfRequestHandler(lock, serverId, timeoutMs, concurrent, resourceHandlers); handler.addHandler(requestHandler); return requestHandler; } /** * Expects exactly the given number of calls to be made concurrently from any combination of the expected calls. Blocks each call until they are explicitly released. * Is not considered "complete" until all expected calls have been received. */ public BlockingHandler blockOnConcurrentExecutionAnyOfToResources(int concurrent, Collection<? extends Resource> expectedCalls) { List<ResourceHandler> resourceHandlers = new ArrayList<ResourceHandler>(); for (Resource call : expectedCalls) { resourceHandlers.add((ResourceHandler) call); } CyclicBarrierAnyOfRequestHandler requestHandler = new CyclicBarrierAnyOfRequestHandler(lock, serverId, timeoutMs, concurrent, resourceHandlers); handler.addHandler(requestHandler); return requestHandler; } /** * Expects the given request to be made. */ public void expectSerialExecution(String expectedCall) { handler.addHandler(new CyclicBarrierRequestHandler(lock, timeoutMs, Collections.singleton(new SimpleResourceHandler(expectedCall)))); } /** * Expects the given request to be made. */ public void expectSerialExecution(Resource expectedCall) { handler.addHandler(new CyclicBarrierRequestHandler(lock, timeoutMs, Collections.singleton((ResourceHandler) expectedCall))); } public void start() { server.start(); running = true; } public void stop() { handler.assertComplete(); running = false; // Stop is very slow, clean it up later EXECUTOR_SERVICE.execute(new Runnable() { @Override public void run() { server.stop(10); } }); } /** * For testing this fixture only. */ void waitForRequests(int requestCount) { handler.waitForRequests(requestCount); } @Override protected void after() { stop(); } private int getPort() { if (!running) { throw new IllegalStateException("Cannot get HTTP port as server is not running."); } return server.getAddress().getPort(); } /** * Represents some HTTP resource. */ public interface Resource { } /** * Allows the test to synchronise with and unblock requests. */ public interface BlockingHandler { /** * Releases the given number of blocked requests. Fails when fewer than the given number of requests are waiting to be released. */ void release(int count); /** * Releases the given request. Fails when the given request is not waiting to be released. */ void release(String path); /** * Releases all requests. Fails when there are requests yet to be received. */ void releaseAll(); /** * Waits for the expected number of concurrent requests to be received. */ void waitForAllPendingCalls(); } }