interop-testing: New client for HTTP/2 interop tests

These tests are for the new HTTP/2 test server (grpc/grpc#8900). They are designed to test client behavior when the server sends unexpected responses during rpcs.
This commit is contained in:
Eric Gribkoff 2016-12-20 15:24:05 -08:00 committed by GitHub
parent ec7d68bb36
commit 24bc205a2b
3 changed files with 480 additions and 0 deletions

View File

@ -68,10 +68,18 @@ task stresstest_client(type: CreateStartScripts) {
defaultJvmOpts = ["-verbose:gc", "-XX:+PrintFlagsFinal"] defaultJvmOpts = ["-verbose:gc", "-XX:+PrintFlagsFinal"]
} }
task http2_client(type: CreateStartScripts) {
mainClassName = "io.grpc.testing.integration.Http2Client"
applicationName = "http2-client"
outputDir = new File(project.buildDir, 'tmp')
classpath = jar.outputs.files + configurations.runtime
}
applicationDistribution.into("bin") { applicationDistribution.into("bin") {
from(test_client) from(test_client)
from(test_server) from(test_server)
from(reconnect_test_client) from(reconnect_test_client)
from(stresstest_client) from(stresstest_client)
from(http2_client)
fileMode = 0755 fileMode = 0755
} }

View File

@ -0,0 +1,400 @@
/*
* Copyright 2016, Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package io.grpc.testing.integration;
import static java.util.concurrent.Executors.newFixedThreadPool;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.protobuf.ByteString;
import io.grpc.ManagedChannel;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.netty.NegotiationType;
import io.grpc.netty.NettyChannelBuilder;
import io.grpc.stub.StreamObserver;
import io.grpc.testing.integration.Messages.Payload;
import io.grpc.testing.integration.Messages.PayloadType;
import io.grpc.testing.integration.Messages.SimpleRequest;
import io.grpc.testing.integration.Messages.SimpleResponse;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Client application for the {@link TestServiceGrpc.TestServiceImplBase} that runs through a series
* of HTTP/2 interop tests. The tests are designed to simulate incorrect behavior on the part of the
* server. Some of the test cases require server-side checks and do not have assertions within the
* client code.
*/
public final class Http2Client {
private static final Logger logger = Logger.getLogger(Http2Client.class.getName());
/**
* The main application allowing this client to be launched from the command line.
*/
public static void main(String[] args) throws Exception {
final Http2Client client = new Http2Client();
client.parseArgs(args);
client.setUp();
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
try {
client.shutdown();
} catch (Exception e) {
logger.log(Level.SEVERE, e.getMessage(), e);
}
}
});
try {
client.run();
} finally {
client.shutdown();
}
}
private String serverHost = "localhost";
private int serverPort = 8080;
private String testCase = Http2TestCases.RST_AFTER_DATA.name();
private Tester tester = new Tester();
private ListeningExecutorService threadpool;
protected ManagedChannel channel;
protected TestServiceGrpc.TestServiceBlockingStub blockingStub;
protected TestServiceGrpc.TestServiceStub asyncStub;
private void parseArgs(String[] args) {
boolean usage = false;
for (String arg : args) {
if (!arg.startsWith("--")) {
System.err.println("All arguments must start with '--': " + arg);
usage = true;
break;
}
String[] parts = arg.substring(2).split("=", 2);
String key = parts[0];
if ("help".equals(key)) {
usage = true;
break;
}
if (parts.length != 2) {
System.err.println("All arguments must be of the form --arg=value");
usage = true;
break;
}
String value = parts[1];
if ("server_host".equals(key)) {
serverHost = value;
} else if ("server_port".equals(key)) {
serverPort = Integer.parseInt(value);
} else if ("test_case".equals(key)) {
testCase = value;
} else {
System.err.println("Unknown argument: " + key);
usage = true;
break;
}
}
if (usage) {
Http2Client c = new Http2Client();
System.out.println(
"Usage: [ARGS...]"
+ "\n"
+ "\n --server_host=HOST Server to connect to. Default " + c.serverHost
+ "\n --server_port=PORT Port to connect to. Default " + c.serverPort
+ "\n --test_case=TESTCASE Test case to run. Default " + c.testCase
+ "\n Valid options:"
+ validTestCasesHelpText()
);
System.exit(1);
}
}
private void setUp() {
channel = createChannel();
blockingStub = TestServiceGrpc.newBlockingStub(channel);
asyncStub = TestServiceGrpc.newStub(channel);
}
private void shutdown() {
try {
if (channel != null) {
channel.shutdownNow();
channel.awaitTermination(1, TimeUnit.SECONDS);
}
} catch (Exception ex) {
throw new RuntimeException(ex);
}
try {
if (threadpool != null) {
threadpool.shutdownNow();
}
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
private void run() {
logger.info("Running test " + testCase);
try {
runTest(Http2TestCases.fromString(testCase));
} catch (RuntimeException ex) {
throw ex;
} catch (Exception ex) {
throw new RuntimeException(ex);
}
logger.info("Test completed.");
}
private void runTest(Http2TestCases testCase) throws Exception {
switch (testCase) {
case RST_AFTER_HEADER:
tester.rstAfterHeader();
break;
case RST_AFTER_DATA:
tester.rstAfterData();
break;
case RST_DURING_DATA:
tester.rstDuringData();
break;
case GOAWAY:
tester.goAway();
break;
case PING:
tester.ping();
break;
case MAX_STREAMS:
tester.maxStreams();
break;
default:
throw new IllegalArgumentException("Unknown test case: " + testCase);
}
}
private class Tester {
private final int timeoutSeconds = 5;
private final int responseSize = 314159;
private final int payloadSize = 271828;
private final SimpleRequest simpleRequest = SimpleRequest.newBuilder()
.setResponseSize(responseSize)
.setResponseType(PayloadType.COMPRESSABLE)
.setPayload(Payload.newBuilder().setBody(ByteString.copyFrom(new byte[payloadSize])))
.build();
final SimpleResponse goldenResponse = SimpleResponse.newBuilder()
.setPayload(Payload.newBuilder()
.setType(PayloadType.COMPRESSABLE)
.setBody(ByteString.copyFrom(new byte[responseSize])))
.build();
private void rstAfterHeader() throws Exception {
try {
blockingStub.unaryCall(simpleRequest);
throw new AssertionError("Expected call to fail");
} catch (StatusRuntimeException ex) {
assertRstStreamReceived(ex.getStatus());
}
}
private void rstAfterData() throws Exception {
// Use async stub to verify data is received.
RstStreamObserver responseObserver = new RstStreamObserver();
asyncStub.unaryCall(simpleRequest, responseObserver);
if (!responseObserver.awaitCompletion(timeoutSeconds, TimeUnit.SECONDS)) {
throw new AssertionError("Operation timed out");
}
if (responseObserver.getResponses().size() != 1) {
throw new AssertionError("Expected one response");
}
if (responseObserver.getError() == null) {
throw new AssertionError("Expected call to fail");
}
assertRstStreamReceived(Status.fromThrowable(responseObserver.getError()));
}
private void rstDuringData() throws Exception {
// Use async stub to verify no data is received.
RstStreamObserver responseObserver = new RstStreamObserver();
asyncStub.unaryCall(simpleRequest, responseObserver);
if (!responseObserver.awaitCompletion(timeoutSeconds, TimeUnit.SECONDS)) {
throw new AssertionError("Operation timed out");
}
if (responseObserver.getResponses().size() != 0) {
throw new AssertionError("Expected zero responses");
}
if (responseObserver.getError() == null) {
throw new AssertionError("Expected call to fail");
}
assertRstStreamReceived(Status.fromThrowable(responseObserver.getError()));
}
private void goAway() throws Exception {
assertResponseEquals(blockingStub.unaryCall(simpleRequest), goldenResponse);
assertResponseEquals(blockingStub.unaryCall(simpleRequest), goldenResponse);
}
private void ping() throws Exception {
assertResponseEquals(blockingStub.unaryCall(simpleRequest), goldenResponse);
}
private void maxStreams() throws Exception {
final int numThreads = 10;
// Preliminary call to ensure MAX_STREAMS setting is received by the client.
assertResponseEquals(blockingStub.unaryCall(simpleRequest), goldenResponse);
threadpool = MoreExecutors.listeningDecorator(newFixedThreadPool(numThreads));
List<ListenableFuture<?>> workerFutures = new ArrayList<ListenableFuture<?>>();
for (int i = 0; i < numThreads; i++) {
workerFutures.add(threadpool.submit(new MaxStreamsWorker(i, simpleRequest)));
}
ListenableFuture<?> f = Futures.allAsList(workerFutures);
f.get(timeoutSeconds, TimeUnit.SECONDS);
}
private class RstStreamObserver implements StreamObserver<SimpleResponse> {
private final CountDownLatch latch = new CountDownLatch(1);
private final List<SimpleResponse> responses = new ArrayList<SimpleResponse>();
private Throwable error;
@Override
public void onNext(SimpleResponse value) {
responses.add(value);
}
@Override
public void onError(Throwable t) {
error = t;
latch.countDown();
}
@Override
public void onCompleted() {
latch.countDown();
}
public List<SimpleResponse> getResponses() {
return responses;
}
public Throwable getError() {
return error;
}
public boolean awaitCompletion(long timeout, TimeUnit unit) throws Exception {
return latch.await(timeout, unit);
}
}
private class MaxStreamsWorker implements Runnable {
int threadNum;
SimpleRequest request;
MaxStreamsWorker(int threadNum, SimpleRequest request) {
this.threadNum = threadNum;
this.request = request;
}
@Override
public void run() {
Thread.currentThread().setName("thread:" + threadNum);
try {
TestServiceGrpc.TestServiceBlockingStub blockingStub =
TestServiceGrpc.newBlockingStub(channel);
assertResponseEquals(blockingStub.unaryCall(simpleRequest), goldenResponse);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
private void assertRstStreamReceived(Status status) {
if (!status.getCode().equals(Status.Code.UNAVAILABLE)) {
throw new AssertionError("Wrong status code. Expected: " + Status.Code.UNAVAILABLE
+ " Received: " + status.getCode());
}
String http2ErrorPrefix = "HTTP/2 error code: NO_ERROR";
if (status.getDescription() == null
|| !status.getDescription().startsWith(http2ErrorPrefix)) {
throw new AssertionError("Wrong HTTP/2 error code. Expected: " + http2ErrorPrefix
+ " Received: " + status.getDescription());
}
}
private void assertResponseEquals(SimpleResponse response, SimpleResponse goldenResponse) {
if (!response.equals(goldenResponse)) {
throw new AssertionError("Incorrect response received");
}
}
}
private ManagedChannel createChannel() {
InetAddress address;
try {
address = InetAddress.getByName(serverHost);
} catch (UnknownHostException ex) {
throw new RuntimeException(ex);
}
return NettyChannelBuilder.forAddress(new InetSocketAddress(address, serverPort))
.negotiationType(NegotiationType.PLAINTEXT)
.build();
}
private static String validTestCasesHelpText() {
StringBuilder builder = new StringBuilder();
for (Http2TestCases testCase : Http2TestCases.values()) {
String strTestcase = testCase.name().toLowerCase();
builder.append("\n ")
.append(strTestcase)
.append(": ")
.append(testCase.description());
}
return builder.toString();
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright 2016, Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package io.grpc.testing.integration;
import com.google.common.base.Preconditions;
/**
* Enum of HTTP/2 interop test cases.
*/
public enum Http2TestCases {
RST_AFTER_HEADER("server resets stream after sending header"),
RST_AFTER_DATA("server resets stream after sending data"),
RST_DURING_DATA("server resets stream in the middle of sending data"),
GOAWAY("server sends goaway after first request and asserts second request uses new connection"),
PING("server sends pings during request and verifies client response"),
MAX_STREAMS("server verifies that the client respects MAX_STREAMS setting");
private final String description;
Http2TestCases(String description) {
this.description = description;
}
/**
* Returns a description of the test case.
*/
public String description() {
return description;
}
/**
* Returns the {@link Http2TestCases} matching the string {@code s}. The
* matching is case insensitive.
*/
public static Http2TestCases fromString(String s) {
Preconditions.checkNotNull(s, "s");
try {
return Http2TestCases.valueOf(s.toUpperCase());
} catch (IllegalArgumentException ex) {
throw new IllegalArgumentException("Invalid test case: " + s);
}
}
}