package me.dinowernli.grpc.polyglot.testing;
import java.io.IOException;
import java.util.Optional;
import java.util.Random;
import com.google.common.base.Throwables;
import io.grpc.Server;
import io.grpc.netty.GrpcSslContexts;
import io.grpc.netty.NettyServerBuilder;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslProvider;
import polyglot.test.TestProto.TestResponse;
import polyglot.test.TestServiceGrpc.TestServiceImplBase;
/**
* Holds a real grpc server for use in tests. The server returns canned responses for a fixed set of
* methods and is optimized for ease of setup in tests (rather than configurability).
*/
public class TestServer {
/** A response sent whenever the test server sees a request to its unary method. */
public static final TestResponse UNARY_SERVER_RESPONSE = TestResponse.newBuilder()
.setMessage("some fancy message")
.build();
/** A response sent whenever the test server sees a request to its streaming method. */
public static final TestResponse STREAMING_SERVER_RESPONSE = TestResponse.newBuilder()
.setMessage("some other message")
.build();
/** A response sent whenever the test server sees a request to its client streaming method. */
public static final TestResponse CLIENT_STREAMING_SERVER_RESPONSE = TestResponse.newBuilder()
.setMessage("woohoo client stream")
.build();
/** A response sent whenever the test server sees a request to its client streaming method. */
public static final TestResponse BIDI_SERVER_RESPONSE = TestResponse.newBuilder()
.setMessage("woohoo bidi stream")
.build();
private static final long NUM_SERVER_START_TRIES = 3;
private static final int MIN_SERVER_PORT = 50_000;
private static final int MAX_SERVER_PORT = 60_000;
private final Server grpcServer;
private final int grpcServerPort;
private final RecordingTestService recordingService;
/**
* Tries a few times to start a server. If this returns, the server has been started. Throws
* {@link RuntimeException} on failure.
*/
public static TestServer createAndStart(Optional<SslContext> sslContext) {
RecordingTestService recordingTestService = new RecordingTestService(
UNARY_SERVER_RESPONSE,
STREAMING_SERVER_RESPONSE,
CLIENT_STREAMING_SERVER_RESPONSE,
BIDI_SERVER_RESPONSE);
Random random = new Random();
for (int i = 0; i < NUM_SERVER_START_TRIES; ++i) {
int port = random.nextInt(MAX_SERVER_PORT - MIN_SERVER_PORT + 1) + MIN_SERVER_PORT;
try {
Server server = tryStartServer(port, recordingTestService, sslContext);
// If we got this far, we have successfully started the server.
return new TestServer(server, port, recordingTestService);
} catch (IOException e) {
// The random port might have been in use, try again...
continue;
}
}
// If we got to here, we didn't manage to start a server.
throw new RuntimeException("Unable to start server after " + NUM_SERVER_START_TRIES + " tries");
}
public TestServer(
Server grpcServer, int grpcServerPort, RecordingTestService recordingTestService) {
this.grpcServer = grpcServer;
this.grpcServerPort = grpcServerPort;
this.recordingService = recordingTestService;
}
public int getGrpcServerPort() {
return grpcServerPort;
}
public RecordingTestService getServiceImpl() {
return recordingService;
}
public void blockingShutdown() {
try {
grpcServer.shutdown().awaitTermination();
} catch (InterruptedException e) {
// Propagate, ensuring the test fails.
Throwables.propagate(e);
}
}
/** An {@link SslContext} for use in unit test servers. Loads our testing certificates. */
public static SslContext serverSslContextForTesting() throws IOException {
return GrpcSslContexts
.forServer(TestUtils.loadServerChainCert(), TestUtils.loadServerKey())
.trustManager(TestUtils.loadRootCaCert())
.sslProvider(SslProvider.OPENSSL)
.build();
}
/** Starts a grpc server on the given port, throws {@link IOException} on failure. */
private static Server tryStartServer(
int port,
TestServiceImplBase testService,
Optional<SslContext> sslContext) throws IOException {
NettyServerBuilder serverBuilder = NettyServerBuilder.forPort(port).addService(testService);
if (sslContext.isPresent()) {
serverBuilder.sslContext(sslContext.get());
}
return serverBuilder.build().start();
}
}