/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.android.sync.test.helpers;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.IdentityHashMap;
import java.util.Map;
import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.net.BaseResource;
import org.mozilla.gecko.sync.net.SyncResourceDelegate;
import org.simpleframework.transport.connect.Connection;
import org.simpleframework.transport.connect.SocketConnection;
/**
* Test helper code to bind <code>MockServer</code> instances to ports.
* <p>
* Maintains a collection of running servers and (by default) throws helpful
* errors if two servers are started "on top" of each other. The
* <b>unchecked</b> exception thrown contains a stack trace pointing to where
* the new server is being created and where the pre-existing server was
* created.
* <p>
* Parses a system property to determine current test port, which is fixed for
* the duration of a test execution.
*/
public class HTTPServerTestHelper {
private static final String LOG_TAG = "HTTPServerTestHelper";
/**
* Port to run HTTP servers on during this test execution.
* <p>
* Lazily initialized on first call to {@link #getTestPort}.
*/
public static Integer testPort = null;
public static final String LOCAL_HTTP_PORT_PROPERTY = "android.sync.local.http.port";
public static final int LOCAL_HTTP_PORT_DEFAULT = 15125;
public final int port;
public Connection connection;
public MockServer server;
/**
* Create a helper to bind <code>MockServer</code> instances.
* <p>
* Use {@link #getTestPort} to determine the port this helper will bind to.
*/
public HTTPServerTestHelper() {
this.port = getTestPort();
}
// For testing only.
protected HTTPServerTestHelper(int port) {
this.port = port;
}
/**
* Lazily initialize test port for this test execution.
* <p>
* Only called from {@link #getTestPort}.
* <p>
* If the test port has not been determined, we try to parse it from a system
* property; if that fails, we return the default test port.
*/
protected synchronized static void ensureTestPort() {
if (testPort != null) {
return;
}
String value = System.getProperty(LOCAL_HTTP_PORT_PROPERTY);
if (value != null) {
try {
testPort = Integer.valueOf(value);
} catch (NumberFormatException e) {
Logger.warn(LOG_TAG, "Got exception parsing local test port; ignoring. ", e);
}
}
if (testPort == null) {
testPort = Integer.valueOf(LOCAL_HTTP_PORT_DEFAULT);
}
}
/**
* The port to which all HTTP servers will be found for the duration of this
* test execution.
* <p>
* We try to parse the port from a system property; if that fails, we return
* the default test port.
*
* @return port number.
*/
public synchronized static int getTestPort() {
if (testPort == null) {
ensureTestPort();
}
return testPort.intValue();
}
/**
* Used to maintain a stack trace pointing to where a server was started.
*/
public static class HTTPServerStartedError extends Error {
private static final long serialVersionUID = -6778447718799087274L;
public final HTTPServerTestHelper httpServer;
public HTTPServerStartedError(HTTPServerTestHelper httpServer) {
this.httpServer = httpServer;
}
}
/**
* Thrown when a server is started "on top" of another server. The cause error
* will be an <code>HTTPServerStartedError</code> with a stack trace pointing
* to where the pre-existing server was started.
*/
public static class HTTPServerAlreadyRunningError extends Error {
private static final long serialVersionUID = -6778447718799087275L;
public HTTPServerAlreadyRunningError(Throwable e) {
super(e);
}
}
/**
* Maintain a hash of running servers. Each value is an error with a stack
* traces pointing to where that server was started.
* <p>
* We don't key on the server itself because each server is a <it>helper</it>
* that may be started many times with different <code>MockServer</code>
* instances.
* <p>
* Synchronize access on the class.
*/
protected static Map<Connection, HTTPServerStartedError> runningServers =
new IdentityHashMap<Connection, HTTPServerStartedError>();
protected synchronized static void throwIfServerAlreadyRunning() {
for (HTTPServerStartedError value : runningServers.values()) {
throw new HTTPServerAlreadyRunningError(value);
}
}
protected synchronized static void registerServerAsRunning(HTTPServerTestHelper httpServer) {
if (httpServer == null || httpServer.connection == null) {
throw new IllegalArgumentException("HTTPServerTestHelper or connection was null; perhaps server has not been started?");
}
HTTPServerStartedError old = runningServers.put(httpServer.connection, new HTTPServerStartedError(httpServer));
if (old != null) {
// Should never happen.
throw old;
}
}
protected synchronized static void unregisterServerAsRunning(HTTPServerTestHelper httpServer) {
if (httpServer == null || httpServer.connection == null) {
throw new IllegalArgumentException("HTTPServerTestHelper or connection was null; perhaps server has not been started?");
}
runningServers.remove(httpServer.connection);
}
public MockServer startHTTPServer(MockServer server, boolean allowMultipleServers) {
BaseResource.rewriteLocalhost = false; // No sense rewriting when we're running the unit tests.
SyncResourceDelegate.connectionTimeoutInMillis = 1000; // No sense waiting a long time for a local connection.
if (!allowMultipleServers) {
throwIfServerAlreadyRunning();
}
try {
this.server = server;
connection = new SocketConnection(server);
SocketAddress address = new InetSocketAddress(port);
connection.connect(address);
registerServerAsRunning(this);
Logger.info(LOG_TAG, "Started HTTP server on port " + port + ".");
} catch (IOException ex) {
Logger.error(LOG_TAG, "Error starting HTTP server on port " + port + ".", ex);
fail(ex.toString());
}
return server;
}
public MockServer startHTTPServer(MockServer server) {
return startHTTPServer(server, false);
}
public MockServer startHTTPServer() {
return startHTTPServer(new MockServer());
}
public void stopHTTPServer() {
try {
if (connection != null) {
unregisterServerAsRunning(this);
connection.close();
}
server = null;
connection = null;
Logger.info(LOG_TAG, "Stopped HTTP server on port " + port + ".");
Logger.debug(LOG_TAG, "Closing connection pool...");
BaseResource.shutdownConnectionManager();
} catch (IOException ex) {
Logger.error(LOG_TAG, "Error stopping HTTP server on port " + port + ".", ex);
fail(ex.toString());
}
}
}