Refactor interop tests to be automated

This commit is contained in:
Stanley Cheung 2020-04-23 15:08:37 -07:00 committed by Stanley Cheung
parent ea88b10600
commit 17fbe35265
8 changed files with 279 additions and 228 deletions

View File

@ -30,6 +30,15 @@ services:
image: grpcweb/node-server image: grpcweb/node-server
ports: ports:
- "9090:9090" - "9090:9090"
node-interop-server:
build:
context: ./
dockerfile: ./net/grpc/gateway/docker/node_interop_server/Dockerfile
depends_on:
- common
image: grpcweb/node-interop-server
ports:
- "7074:7074"
envoy: envoy:
build: build:
context: ./ context: ./
@ -95,12 +104,12 @@ services:
image: grpcweb/binary-client image: grpcweb/binary-client
ports: ports:
- "8081:8081" - "8081:8081"
interop: interop-client:
build: build:
context: ./ context: ./
dockerfile: ./net/grpc/gateway/docker/interop/Dockerfile dockerfile: ./net/grpc/gateway/docker/interop_client/Dockerfile
depends_on: depends_on:
- prereqs - prereqs
image: grpcweb/interop image: grpcweb/interop-client
ports: ports:
- "8081:8081" - "8081:8081"

View File

@ -0,0 +1,37 @@
# Copyright 2018 Google LLC
#
# 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
#
# https://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.
FROM grpcweb/common
WORKDIR /github/grpc-node
RUN git clone https://github.com/grpc/grpc-node . && \
git submodule update --init --recursive
RUN cd packages/grpc-native-core && \
npm install --build-from-source --unsafe-perm && \
npm link
RUN cd packages/proto-loader && \
npm install @types/mocha && \
npm install --unsafe-perm
WORKDIR /github/grpc-node/test
RUN npm install node-pre-gyp && \
npm install && \
npm link grpc
EXPOSE 7074
CMD ["node", "--require", "./fixtures/native_native", "./interop/interop_server.js", "--port=7074"]

View File

@ -21,6 +21,14 @@ cd "${REPO_DIR}"
./scripts/init_submodules.sh ./scripts/init_submodules.sh
make clean make clean
# These programs need to be already installed
progs=(docker docker-compose npm curl)
for p in "${progs[@]}"
do
command -v "$p" > /dev/null 2>&1 || \
{ echo >&2 "$p is required but not installed. Aborting."; exit 1; }
done
# Lint bazel files. # Lint bazel files.
BUILDIFIER_VERSION=1.0.0 BUILDIFIER_VERSION=1.0.0
BUILDIFIER_SUFFIX="" BUILDIFIER_SUFFIX=""
@ -33,17 +41,6 @@ chmod +x "./buildifier"
./buildifier --mode=check --lint=warn --warnings=all -r bazel javascript net ./buildifier --mode=check --lint=warn --warnings=all -r bazel javascript net
rm ./buildifier rm ./buildifier
# These programs need to be already installed
progs=(docker docker-compose npm curl)
for p in "${progs[@]}"
do
command -v "$p" > /dev/null 2>&1 || \
{ echo >&2 "$p is required but not installed. Aborting."; exit 1; }
done
# Build all relevant docker images. They should all build successfully.
docker-compose -f advanced.yml build
# Run all bazel unit tests # Run all bazel unit tests
BAZEL_VERSION=2.2.0 BAZEL_VERSION=2.2.0
BAZEL_OS="linux" BAZEL_OS="linux"
@ -60,12 +57,19 @@ $HOME/bin/bazel test \
//javascript/net/grpc/web/... \ //javascript/net/grpc/web/... \
//net/grpc/gateway/examples/... //net/grpc/gateway/examples/...
# Build the protoc plugin
make plugin
# Build the grpc-web npm package # Build the grpc-web npm package
cd packages/grpc-web && \ cd packages/grpc-web && \
npm install && \ npm install && \
npm run build && \ npm run build && \
npm link && \
cd ../.. cd ../..
# Build all relevant docker images. They should all build successfully.
docker-compose -f advanced.yml build
# Bring up the Echo server and the Envoy proxy (in background). # Bring up the Echo server and the Envoy proxy (in background).
# The 'sleep' seems necessary for the docker containers to be fully up # The 'sleep' seems necessary for the docker containers to be fully up
# and listening before we test the with curl requests # and listening before we test the with curl requests
@ -80,3 +84,26 @@ docker-compose down
# Run unit tests from npm package # Run unit tests from npm package
docker run --rm grpcweb/prereqs /bin/bash \ docker run --rm grpcweb/prereqs /bin/bash \
/github/grpc-web/scripts/docker-run-tests.sh /github/grpc-web/scripts/docker-run-tests.sh
# Run interop tests
pid1=$(docker run -d -v $(pwd)/test/interop/envoy.yaml:/etc/envoy/envoy.yaml:ro \
--network=host -p 8080:8080 envoyproxy/envoy:v1.14.1)
pid2=$(docker run -d --network=host -p 7074:7074 grpcweb/node-interop-server)
cd test/interop && \
npm install && \
npm link grpc-web && \
protoc -I=../.. src/proto/grpc/testing/test.proto \
src/proto/grpc/testing/empty.proto \
src/proto/grpc/testing/messages.proto \
--plugin=protoc-gen-grpc-web=$(pwd)/../../javascript/net/grpc/web/protoc-gen-grpc-web \
--js_out=import_style=commonjs:. \
--grpc-web_out=import_style=commonjs,mode=grpcwebtext:. && \
npm test && \
cd ../..
# Clean up
docker rm -f $pid1
docker rm -f $pid2
git clean -f -d -x
echo 'Completed'

3
test/interop/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules/
package-lock.json
src/

View File

@ -9,14 +9,20 @@ for details about gRPC interop tests in general and the list of test cases.
Run interop tests Run interop tests
----------------- -----------------
### Build some docker images
```sh
$ cd grpc-web
$ docker-compose -f advanced.yml build common prereqs node-interop-server interop-client
```
### Run the Node interop server ### Run the Node interop server
An interop server implemented in Node is hosted in the `grpc/grpc-node` repo. An interop server implemented in Node is hosted in the `grpc/grpc-node` repo.
There might be a bit of set up you need to do before running the command below.
```sh ```sh
$ cd grpc-node/test $ docker run -d --network=host -p 7074:7074 grpcweb/node-interop-server
$ node --require ./fixtures/native_native interop/interop_server.js --port=7074
``` ```
@ -26,19 +32,25 @@ An `envoy.yaml` file is provided in this directory to direct traffic for these
tests. tests.
```sh ```sh
$ cd grpc-web $ docker run -d -v $(pwd)/test/interop/envoy.yaml:/etc/envoy/envoy.yaml:ro \
$ docker run -it --rm -v $(pwd)/test/interop/envoy.yaml:/etc/envoy/envoy.yaml:ro \ --network=host -p 8080:8080 envoyproxy/envoy:v1.14.1
--network=host -p 8080:8080 envoyproxy/envoy:latest
``` ```
### Run the gRPC-Web browser client ### Run the gRPC-Web browser client
You can either run the interop client as `npm test`, like this:
```sh ```sh
$ cd grpc-web $ cd test/interop
$ docker-compose -f advanced.yml build common prereqs interop $ npm install
$ docker-compose -f advanced.yml up interop $ npm test
```
Or from the browser:
```sh
$ docker-compose -f advanced.yml up interop-client
``` ```
Open up the browser and go to `http://localhost:8081/index.html` and open up Open up the browser and go to `http://localhost:8081/index.html` and open up

View File

@ -25,226 +25,185 @@ const {SimpleRequest,
require('./src/proto/grpc/testing/messages_pb.js'); require('./src/proto/grpc/testing/messages_pb.js');
const {TestServiceClient} = const {TestServiceClient} =
require('./src/proto/grpc/testing/test_grpc_web_pb.js'); require('./src/proto/grpc/testing/test_grpc_web_pb.js');
var assert = require('assert');
const grpc = {}; const grpc = {};
grpc.web = require('grpc-web'); grpc.web = require('grpc-web');
var testService = new TestServiceClient( const SERVER_HOST = 'http://localhost:8080';
'http://'+window.location.hostname+':8080', null, null);
function doEmptyUnary() { function multiDone(done, count) {
return new Promise((resolve, reject) => { return function() {
testService.emptyCall(new Empty(), null, (err, response) => { count -= 1;
if (err) { if (count <= 0) {
reject('EmptyUnary failed: Received unexpected error "'+ done();
err.message+'"'); }
return; };
} else { }
if (!(response instanceof Empty)) {
reject('EmptyUnary failed: Response not of Empty type'); function doEmptyUnary(done) {
return; var testService = new TestServiceClient(SERVER_HOST, null, null);
} testService.emptyCall(new Empty(), null, (err, response) => {
resolve('EmptyUnary: passed'); assert.ifError(err);
} assert(response instanceof Empty);
}); done();
}); });
} }
function doLargeUnary() { function doLargeUnary(done) {
return new Promise((resolve, reject) => { var testService = new TestServiceClient(SERVER_HOST, null, null);
var req = new SimpleRequest(); var req = new SimpleRequest();
var size = 314159; var size = 314159;
var payload = new Payload(); var payload = new Payload();
payload.setBody('0'.repeat(271828)); payload.setBody('0'.repeat(271828));
req.setPayload(payload); req.setPayload(payload);
req.setResponseSize(size); req.setResponseSize(size);
testService.unaryCall(req, null, (err, response) => { testService.unaryCall(req, null, (err, response) => {
if (err) { assert.ifError(err);
reject('LargeUnary failed: Received unexpected error "'+ assert.equal(response.getPayload().getBody().length, size);
err.message+'"'); done();
return;
} else {
if (response.getPayload().getBody().length != size) {
rejecet('LargeUnary failed: Received incorrect size payload');
return;
}
resolve('LargeUnary: passed');
}
});
}); });
} }
function doServerStreaming() { function doServerStreaming(done) {
return new Promise((resolve, reject) => { var testService = new TestServiceClient(SERVER_HOST, null, null);
var sizes = [31415, 9, 2653, 58979]; var sizes = [31415, 9, 2653, 58979];
var responseParams = sizes.map((size, idx) => { var responseParams = sizes.map((size, idx) => {
var param = new ResponseParameters(); var param = new ResponseParameters();
param.setSize(size); param.setSize(size);
param.setIntervalUs(idx * 10); param.setIntervalUs(idx * 10);
return param; return param;
});
var req = new StreamingOutputCallRequest();
req.setResponseParametersList(responseParams);
var stream = testService.streamingOutputCall(req);
done = multiDone(done, sizes.length);
var numCallbacks = 0;
stream.on('data', (response) => {
assert.equal(response.getPayload().getBody().length, sizes[numCallbacks]);
numCallbacks++;
done();
});
}
function doCustomMetadata(done) {
var testService = new TestServiceClient(SERVER_HOST, null, null);
done = multiDone(done, 3);
var req = new SimpleRequest();
const size = 314159;
const ECHO_INITIAL_KEY = 'x-grpc-test-echo-initial';
const ECHO_INITIAL_VALUE = 'test_initial_metadata_value';
const ECHO_TRAILING_KEY = 'x-grpc-test-echo-trailing-bin';
const ECHO_TRAILING_VALUE = 0xababab;
var payload = new Payload();
payload.setBody('0'.repeat(271828));
req.setPayload(payload);
req.setResponseSize(size);
var call = testService.unaryCall(req, {
[ECHO_INITIAL_KEY]: ECHO_INITIAL_VALUE,
[ECHO_TRAILING_KEY]: ECHO_TRAILING_VALUE
}, (err, response) => {
assert.ifError(err);
assert.equal(response.getPayload().getBody().length, size);
done();
});
call.on('metadata', (metadata) => {
assert(ECHO_INITIAL_KEY in metadata);
assert.equal(metadata[ECHO_INITIAL_KEY], ECHO_INITIAL_VALUE);
done();
});
call.on('status', (status) => {
assert('metadata' in status);
assert(ECHO_TRAILING_KEY in status.metadata);
assert.equal(status.metadata[ECHO_TRAILING_KEY], ECHO_TRAILING_VALUE);
done();
});
}
function doStatusCodeAndMessage(done) {
var testService = new TestServiceClient(SERVER_HOST, null, null);
var req = new SimpleRequest();
const TEST_STATUS_MESSAGE = 'test status message';
const echoStatus = new EchoStatus();
echoStatus.setCode(2);
echoStatus.setMessage(TEST_STATUS_MESSAGE);
req.setResponseStatus(echoStatus);
testService.unaryCall(req, {}, (err, response) => {
assert(err);
assert('code' in err);
assert('message' in err);
assert.equal(err.code, 2);
assert.equal(err.message, TEST_STATUS_MESSAGE);
done();
});
}
function doUnimplementedMethod(done) {
var testService = new TestServiceClient(SERVER_HOST, null, null);
testService.unimplementedCall(new Empty(), {}, (err, response) => {
assert(err);
assert('code' in err);
assert.equal(err.code, 12);
done();
});
}
var testCases = {
'empty_unary': doEmptyUnary,
'large_unary': doLargeUnary,
'server_streaming': doServerStreaming,
'custom_metadata': doCustomMetadata,
'status_code_and_message': doStatusCodeAndMessage,
'unimplemented_method': doUnimplementedMethod
};
if (typeof window === 'undefined') {
console.log('Running from Node...');
// Fill in XHR runtime
global.XMLHttpRequest = require("xhr2");
describe('grpc-web interop tests', function() {
Object.keys(testCases).forEach((testCase) => {
it('should pass '+testCase, testCases[testCase]);
}); });
});
} else {
console.log('Running from browser...');
var req = new StreamingOutputCallRequest(); Object.keys(testCases).forEach((testCase) => {
req.setResponseParametersList(responseParams); var test = testCases[testCase];
var stream = testService.streamingOutputCall(req); var doneCalled = false;
test((err) => {
var numCallbacks = 0; if (err) {
stream.on('data', (response) => { throw err;
if (response.getPayload().getBody().length != sizes[numCallbacks]) { } else {
reject('ServerStreaming failed: Received incorrect size payload'); doneCalled = true;
return; console.log(testCase+': passed');
} }
numCallbacks++;
}); });
// TODO(stanleycheung): is there a better way to do this?
setTimeout(() => { setTimeout(() => {
if (numCallbacks != sizes.length) { if (!doneCalled) {
reject('ServerStreaming failed: data callback called '+ throw testCase+': failed. Not all done() are called';
numCallbacks+' times. Should be '+sizes.length);
return;
} }
resolve('ServerStreaming: passed'); }, 500);
}, 200);
}); });
} }
function doCustomMetadata() {
return new Promise((resolve, reject) => {
var req = new SimpleRequest();
const size = 314159;
const ECHO_INITIAL_KEY = 'x-grpc-test-echo-initial';
const ECHO_INITIAL_VALUE = 'test_initial_metadata_value';
const ECHO_TRAILING_KEY = 'x-grpc-test-echo-trailing-bin';
const ECHO_TRAILING_VALUE = 0xababab;
var payload = new Payload();
payload.setBody('0'.repeat(271828));
req.setPayload(payload);
req.setResponseSize(size);
var call = testService.unaryCall(req, {
[ECHO_INITIAL_KEY]: ECHO_INITIAL_VALUE,
[ECHO_TRAILING_KEY]: ECHO_TRAILING_VALUE
}, (err, response) => {
if (err) {
reject('CustomMetadata failed: Received unexpected error "'+
err.message+'"');
return;
} else {
if (response.getPayload().getBody().length != size) {
rejecet('CustomMetadata failed: Received incorrect size payload');
return;
}
}
});
var metadataCallbackVerified = false;
var statusCallbackVerified = false;
call.on('metadata', (metadata) => {
if (!(ECHO_INITIAL_KEY in metadata)) {
reject('CustomMetadata failed: initial metadata does not '+
'contain '+ECHO_INITIAL_KEY);
} else if (metadata[ECHO_INITIAL_KEY] != ECHO_INITIAL_VALUE) {
reject('CustomMetadata failed: initial metadata value for '+
ECHO_INITIAL_KEY+' is incorrect');
} else {
metadataCallbackVerified = true;
}
});
call.on('status', (status) => {
if (!('metadata' in status)) {
reject('CustomMetadata failed: status callback does not '+
'contain metadata');
} else if (!(ECHO_TRAILING_KEY in status.metadata)) {
reject('CustomMetadata failed: trailing metadata does not '+
'contain '+ECHO_TRAILING_KEY);
} else if (status.metadata[ECHO_TRAILING_KEY] != ECHO_TRAILING_VALUE) {
reject('CustomMetadata failed: trailing metadata value for '+
ECHO_TRAILING_KEY+' is incorrect');
} else {
statusCallbackVerified = true;
}
});
setTimeout(() => {
if (!metadataCallbackVerified || !statusCallbackVerified) {
reject('CustomMetadata failed: some callback failed to verify');
}
resolve('CustomMetadta passed');
}, 200);
});
}
function doStatusCodeAndMessage() {
return new Promise((resolve, reject) => {
var req = new SimpleRequest();
const TEST_STATUS_MESSAGE = 'test status message';
const echoStatus = new EchoStatus();
echoStatus.setCode(2);
echoStatus.setMessage(TEST_STATUS_MESSAGE);
req.setResponseStatus(echoStatus);
testService.unaryCall(req, {}, (err, response) => {
if (err) {
if (!('code' in err)) {
reject('StatusCodeAndMessage failed: status callback does not '+
'contain code');
} else if (!('message' in err)) {
reject('StatusCodeAndMessage failed: status callback does not '+
'contain message');
} else if (err.code != 2) {
reject('StatusCodeAndMessage failed: status code is not 2, is '+
err.code);
} else if (err.message != TEST_STATUS_MESSAGE) {
reject('StatusCodeAndMessage failed: status mesage is not '+
TEST_STATUS_MESSAGE+', is '+err.message);
} else {
resolve('StatusCodeAndMessage passed');
}
} else {
reject('StatusCodeAndMessage failed: should not have received a '+
'proper response');
}
});
});
}
function doUnimplementedMethod() {
return new Promise((resolve, reject) => {
testService.unimplementedCall(new Empty(), {}, (err, response) => {
if (err) {
if (!('code' in err)) {
reject('UnimplementedMethod failed: status callback does not '+
'contain code');
} else if (!('message' in err)) {
reject('UnimplementedMethod failed: status callback does not '+
'contain message');
} else if (err.code != 12) {
reject('UnimplementedMethod failed: status code is not 12'+
'(UNIMPLEMENTED), is '+ err.code);
} else {
resolve('UnimplementedMethod passed');
}
} else {
reject('UnimplementedMethod failed: should not have received a '+
'proper respoonse');
}
});
});
}
var testCases = [doEmptyUnary, doLargeUnary, doServerStreaming,
doCustomMetadata, doStatusCodeAndMessage,
doUnimplementedMethod];
testCases.reduce((promiseChain, currentTask) => {
return promiseChain.then(() => {
return currentTask().then(console.log);
}).catch(console.error);
}, Promise.resolve());

View File

@ -3,13 +3,17 @@
"version": "0.1.0", "version": "0.1.0",
"description": "gRPC-Web Interop Test Client", "description": "gRPC-Web Interop Test Client",
"license": "Apache-2.0", "license": "Apache-2.0",
"scripts": {
"test": "mocha -b --timeout 500 client.js"
},
"dependencies": { "dependencies": {
"google-protobuf": "^3.6.1", "google-protobuf": "~3.11.4",
"grpc-web": "^1.0.0" "grpc-web": "~1.0.0"
}, },
"devDependencies": { "devDependencies": {
"browserify": "^16.2.2", "mocha": "~7.1.1",
"webpack": "^4.16.5", "webpack": "~4.43.0",
"webpack-cli": "^3.1.0" "webpack-cli": "~3.3.11",
"xhr2": "~0.2.0"
} }
} }