Add interop spec and interop tests

This commit is contained in:
Stanley Cheung 2020-04-06 16:24:21 -07:00 committed by Stanley Cheung
parent 0bf05fd26a
commit ea88b10600
12 changed files with 838 additions and 0 deletions

View File

@ -95,3 +95,12 @@ services:
image: grpcweb/binary-client
ports:
- "8081:8081"
interop:
build:
context: ./
dockerfile: ./net/grpc/gateway/docker/interop/Dockerfile
depends_on:
- prereqs
image: grpcweb/interop
ports:
- "8081:8081"

View File

@ -0,0 +1,82 @@
gRPC-Web Interop Tests
======================
This document describes the set of tests any gRPC-Web clients or proxies need
to implement. The proto definition for the messages and RPCs we are using for
the tests can be found
[here](src/proto/grpc/testing/test.proto).
The canonical set of interop tests was defined in the main
[grpc/grpc repo](https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md).
Here in gRPC-Web, we will only implement a subset of tests that's relevant to
gRPC-Web. For example, we will not be implementing any tests involving
client-streaming or bidi-streaming for now. On the other hand, there are
gRPC-Web specific tests that we will add here.
```
gRPC-Web Client <--> Proxy <--> gRPC Service
```
The idea is that we should be able to swap out any of the 3 components above
and all the interop tests should still pass.
This repository will provide a canonical implementation of the interop test
suite using the Javascript client, Envoy and a gRPC service implemented in
Node.
For any new gRPC-Web client implementation, you need to swap out the JS
client and make sure all tests still pass.
For any in-process proxies implementation, you need to swap out the proxy
and the service as a unit and make sure the standard JS client will still
pass the tests.
List of Tests
-------------
| Test Name | grpc-web-text Mode | grpc-web Binary mode |
| --------- |:------------------:|:--------------------:|
| empty_unary | &#10003; | &#10003; |
| cacheable_unary | TBD | TBD |
| large_unary | &#10003; | &#10003; |
| client_compressed_unary | TBD | TBD |
| server_compressed_unary | TBD | TBD |
| client_streaming | &#10007; | &#10007; |
| client_compressed_streaming | &#10007; | &#10007; |
| server_streaming | &#10003; | &#10007; |
| server_compressed_streaming | TBD | &#10007; |
| ping_pong | &#10007; | &#10007; |
| empty_stream | &#10007; | &#10007; |
| compute_engine_creds | TBD | TBD |
| jwt_token_creds | TBD | TBD |
| oauth2_auth_token | TBD | TBD |
| per_rpc_creds | TBD | TBD |
| google_default_credentials | TBD | TBD |
| compute_engine_channel_credentials | TBD | TBD |
| custom_metadata | &#10003; | &#10003; |
| status_code_and_message | &#10003; | &#10003; |
| special_status_message | &#10003; | &#10003; |
| unimplemented_method | &#10003; | &#10003; |
| unimplemented_service | &#10003; | &#10003; |
| cancel_after_begin | &#10007; | &#10007; |
| cancel_after_first_response | &#10007; | &#10007; |
| timeout_on_sleeping_server | &#10007; | &#10007; |
gRPC-Web specific considerations
--------------------------------
### Text vs Binary mode
As mentioned in the table above, client needs to be tested in both the text
format `application/grpc-web-text` and the binary mode
`application/grpc-web+proto`. The latter we don't need to test any streaming
methods.
### CORS and other web specific scenarios
We may add specific tests to account for web-related scenarios like CORS
handling, etc. Mostly these are to test the connection between the browser
client and the proxy.

View File

@ -0,0 +1,35 @@
# 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/prereqs
WORKDIR /github/grpc-web/test/interop
COPY ./test/interop .
RUN protoc -I=../.. src/proto/grpc/testing/test.proto \
src/proto/grpc/testing/empty.proto src/proto/grpc/testing/messages.proto \
--js_out=import_style=commonjs:. \
--grpc-web_out=import_style=commonjs,mode=grpcwebtext:.
RUN npm install && \
npm link grpc-web && \
npx webpack && \
cp index.html /var/www/html && \
cp dist/main.js /var/www/html/dist
WORKDIR /var/www/html
EXPOSE 8081
CMD ["python", "-m", "SimpleHTTPServer", "8081"]

View File

@ -0,0 +1,28 @@
// Copyright 2015 gRPC 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.
syntax = "proto3";
package grpc.testing;
// An empty message that you can re-use to avoid defining duplicated empty
// messages in your project. A typical example is to use it as argument or the
// return value of a service API. For instance:
//
// service Foo {
// rpc Bar (grpc.testing.Empty) returns (grpc.testing.Empty) { };
// };
//
message Empty {}

View File

@ -0,0 +1,209 @@
// Copyright 2015-2016 gRPC 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.
// Message definitions to be used by integration test service definitions.
syntax = "proto3";
package grpc.testing;
// TODO(dgq): Go back to using well-known types once
// https://github.com/grpc/grpc/issues/6980 has been fixed.
// import "google/protobuf/wrappers.proto";
message BoolValue {
// The bool value.
bool value = 1;
}
// The type of payload that should be returned.
enum PayloadType {
// Compressable text format.
COMPRESSABLE = 0;
}
// A block of data, to simply increase gRPC message size.
message Payload {
// The type of data in body.
PayloadType type = 1;
// Primary contents of payload.
bytes body = 2;
}
// A protobuf representation for grpc status. This is used by test
// clients to specify a status that the server should attempt to return.
message EchoStatus {
int32 code = 1;
string message = 2;
}
// The type of route that a client took to reach a server w.r.t. gRPCLB.
// The server must fill in "fallback" if it detects that the RPC reached
// the server via the "gRPCLB fallback" path, and "backend" if it detects
// that the RPC reached the server via "gRPCLB backend" path (i.e. if it got
// the address of this server from the gRPCLB server BalanceLoad RPC). Exactly
// how this detection is done is context and server dependent.
enum GrpclbRouteType {
// Server didn't detect the route that a client took to reach it.
GRPCLB_ROUTE_TYPE_UNKNOWN = 0;
// Indicates that a client reached a server via gRPCLB fallback.
GRPCLB_ROUTE_TYPE_FALLBACK = 1;
// Indicates that a client reached a server as a gRPCLB-given backend.
GRPCLB_ROUTE_TYPE_BACKEND = 2;
}
// Unary request.
message SimpleRequest {
// Desired payload type in the response from the server.
// If response_type is RANDOM, server randomly chooses one from other formats.
PayloadType response_type = 1;
// Desired payload size in the response from the server.
int32 response_size = 2;
// Optional input payload sent along with the request.
Payload payload = 3;
// Whether SimpleResponse should include username.
bool fill_username = 4;
// Whether SimpleResponse should include OAuth scope.
bool fill_oauth_scope = 5;
// Whether to request the server to compress the response. This field is
// "nullable" in order to interoperate seamlessly with clients not able to
// implement the full compression tests by introspecting the call to verify
// the response's compression status.
BoolValue response_compressed = 6;
// Whether server should return a given status
EchoStatus response_status = 7;
// Whether the server should expect this request to be compressed.
BoolValue expect_compressed = 8;
// Whether SimpleResponse should include server_id.
bool fill_server_id = 9;
// Whether SimpleResponse should include grpclb_route_type.
bool fill_grpclb_route_type = 10;
}
// Unary response, as configured by the request.
message SimpleResponse {
// Payload to increase message size.
Payload payload = 1;
// The user the request came from, for verifying authentication was
// successful when the client expected it.
string username = 2;
// OAuth scope.
string oauth_scope = 3;
// Server ID. This must be unique among different server instances,
// but the same across all RPC's made to a particular server instance.
string server_id = 4;
// gRPCLB Path.
GrpclbRouteType grpclb_route_type = 5;
// Server hostname.
string hostname = 6;
}
// Client-streaming request.
message StreamingInputCallRequest {
// Optional input payload sent along with the request.
Payload payload = 1;
// Whether the server should expect this request to be compressed. This field
// is "nullable" in order to interoperate seamlessly with servers not able to
// implement the full compression tests by introspecting the call to verify
// the request's compression status.
BoolValue expect_compressed = 2;
// Not expecting any payload from the response.
}
// Client-streaming response.
message StreamingInputCallResponse {
// Aggregated size of payloads received from the client.
int32 aggregated_payload_size = 1;
}
// Configuration for a particular response.
message ResponseParameters {
// Desired payload sizes in responses from the server.
int32 size = 1;
// Desired interval between consecutive responses in the response stream in
// microseconds.
int32 interval_us = 2;
// Whether to request the server to compress the response. This field is
// "nullable" in order to interoperate seamlessly with clients not able to
// implement the full compression tests by introspecting the call to verify
// the response's compression status.
BoolValue compressed = 3;
}
// Server-streaming request.
message StreamingOutputCallRequest {
// Desired payload type in the response from the server.
// If response_type is RANDOM, the payload from each response in the stream
// might be of different types. This is to simulate a mixed type of payload
// stream.
PayloadType response_type = 1;
// Configuration for each expected response message.
repeated ResponseParameters response_parameters = 2;
// Optional input payload sent along with the request.
Payload payload = 3;
// Whether server should return a given status
EchoStatus response_status = 7;
}
// Server-streaming response, as configured by the request and parameters.
message StreamingOutputCallResponse {
// Payload to increase response size.
Payload payload = 1;
}
// For reconnect interop test only.
// Client tells server what reconnection parameters it used.
message ReconnectParams {
int32 max_reconnect_backoff_ms = 1;
}
// For reconnect interop test only.
// Server tells client whether its reconnects are following the spec and the
// reconnect backoffs it saw.
message ReconnectInfo {
bool passed = 1;
repeated int32 backoff_ms = 2;
}
message LoadBalancerStatsRequest {
// Request stats for the next num_rpcs sent by client.
int32 num_rpcs = 1;
// If num_rpcs have not completed within timeout_sec, return partial results.
int32 timeout_sec = 2;
}
message LoadBalancerStatsResponse {
// The number of completed RPCs for each peer.
map<string, int32> rpcs_by_peer = 1;
// The number of RPCs that failed to record a remote peer.
int32 num_failures = 2;
}

View File

@ -0,0 +1,86 @@
// Copyright 2015-2016 gRPC 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.
// An integration test service that covers all the method signature permutations
// of unary/streaming requests/responses.
syntax = "proto3";
import "src/proto/grpc/testing/empty.proto";
import "src/proto/grpc/testing/messages.proto";
package grpc.testing;
// A simple service to test the various types of RPCs and experiment with
// performance with various types of payload.
service TestService {
// One empty request followed by one empty response.
rpc EmptyCall(grpc.testing.Empty) returns (grpc.testing.Empty);
// One request followed by one response.
rpc UnaryCall(SimpleRequest) returns (SimpleResponse);
// One request followed by one response. Response has cache control
// headers set such that a caching HTTP proxy (such as GFE) can
// satisfy subsequent requests.
rpc CacheableUnaryCall(SimpleRequest) returns (SimpleResponse);
// One request followed by a sequence of responses (streamed download).
// The server returns the payload with client desired type and sizes.
rpc StreamingOutputCall(StreamingOutputCallRequest)
returns (stream StreamingOutputCallResponse);
// A sequence of requests followed by one response (streamed upload).
// The server returns the aggregated size of client payload as the result.
rpc StreamingInputCall(stream StreamingInputCallRequest)
returns (StreamingInputCallResponse);
// A sequence of requests with each request served by the server immediately.
// As one request could lead to multiple responses, this interface
// demonstrates the idea of full duplexing.
rpc FullDuplexCall(stream StreamingOutputCallRequest)
returns (stream StreamingOutputCallResponse);
// A sequence of requests followed by a sequence of responses.
// The server buffers all the client requests and then serves them in order. A
// stream of responses are returned to the client when the server starts with
// first request.
rpc HalfDuplexCall(stream StreamingOutputCallRequest)
returns (stream StreamingOutputCallResponse);
// The test server will not implement this method. It will be used
// to test the behavior when clients call unimplemented methods.
rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty);
}
// A simple service NOT implemented at servers so clients can test for
// that case.
service UnimplementedService {
// A call that no server should implement
rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty);
}
// A service used to control reconnect server.
service ReconnectService {
rpc Start(grpc.testing.ReconnectParams) returns (grpc.testing.Empty);
rpc Stop(grpc.testing.Empty) returns (grpc.testing.ReconnectInfo);
}
// A service used to obtain stats for verifying LB behavior.
service LoadBalancerStatsService {
// Gets the backend distribution for RPCs sent by a test client.
rpc GetClientStats(LoadBalancerStatsRequest)
returns (LoadBalancerStatsResponse) {}
}

45
test/interop/README.md Normal file
View File

@ -0,0 +1,45 @@
gRPC-Web Interop Tests
======================
See the
[main doc](https://github.com/grpc/grpc-web/blob/master/interop-test-descriptions.md)
for details about gRPC interop tests in general and the list of test cases.
Run interop tests
-----------------
### Run the Node interop server
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
$ cd grpc-node/test
$ node --require ./fixtures/native_native interop/interop_server.js --port=7074
```
### Run the Envoy proxy
An `envoy.yaml` file is provided in this directory to direct traffic for these
tests.
```sh
$ cd grpc-web
$ docker run -it --rm -v $(pwd)/test/interop/envoy.yaml:/etc/envoy/envoy.yaml:ro \
--network=host -p 8080:8080 envoyproxy/envoy:latest
```
### Run the gRPC-Web browser client
```sh
$ cd grpc-web
$ docker-compose -f advanced.yml build common prereqs interop
$ docker-compose -f advanced.yml up interop
```
Open up the browser and go to `http://localhost:8081/index.html` and open up
the console.

250
test/interop/client.js Normal file
View File

@ -0,0 +1,250 @@
/**
*
* 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.
*
*/
const {Empty} = require('./src/proto/grpc/testing/empty_pb.js');
const {SimpleRequest,
StreamingOutputCallRequest,
EchoStatus,
Payload,
ResponseParameters} =
require('./src/proto/grpc/testing/messages_pb.js');
const {TestServiceClient} =
require('./src/proto/grpc/testing/test_grpc_web_pb.js');
const grpc = {};
grpc.web = require('grpc-web');
var testService = new TestServiceClient(
'http://'+window.location.hostname+':8080', null, null);
function doEmptyUnary() {
return new Promise((resolve, reject) => {
testService.emptyCall(new Empty(), null, (err, response) => {
if (err) {
reject('EmptyUnary failed: Received unexpected error "'+
err.message+'"');
return;
} else {
if (!(response instanceof Empty)) {
reject('EmptyUnary failed: Response not of Empty type');
return;
}
resolve('EmptyUnary: passed');
}
});
});
}
function doLargeUnary() {
return new Promise((resolve, reject) => {
var req = new SimpleRequest();
var size = 314159;
var payload = new Payload();
payload.setBody('0'.repeat(271828));
req.setPayload(payload);
req.setResponseSize(size);
testService.unaryCall(req, null, (err, response) => {
if (err) {
reject('LargeUnary failed: Received unexpected error "'+
err.message+'"');
return;
} else {
if (response.getPayload().getBody().length != size) {
rejecet('LargeUnary failed: Received incorrect size payload');
return;
}
resolve('LargeUnary: passed');
}
});
});
}
function doServerStreaming() {
return new Promise((resolve, reject) => {
var sizes = [31415, 9, 2653, 58979];
var responseParams = sizes.map((size, idx) => {
var param = new ResponseParameters();
param.setSize(size);
param.setIntervalUs(idx * 10);
return param;
});
var req = new StreamingOutputCallRequest();
req.setResponseParametersList(responseParams);
var stream = testService.streamingOutputCall(req);
var numCallbacks = 0;
stream.on('data', (response) => {
if (response.getPayload().getBody().length != sizes[numCallbacks]) {
reject('ServerStreaming failed: Received incorrect size payload');
return;
}
numCallbacks++;
});
// TODO(stanleycheung): is there a better way to do this?
setTimeout(() => {
if (numCallbacks != sizes.length) {
reject('ServerStreaming failed: data callback called '+
numCallbacks+' times. Should be '+sizes.length);
return;
}
resolve('ServerStreaming: passed');
}, 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());

44
test/interop/envoy.yaml Normal file
View File

@ -0,0 +1,44 @@
admin:
access_log_path: /tmp/admin_access.log
address:
socket_address: { address: 0.0.0.0, port_value: 9901 }
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
- filters:
- name: envoy.http_connection_manager
config:
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route:
cluster: interop_service
max_grpc_timeout: 0s
cors:
allow_origin_string_match:
- prefix: "*"
allow_methods: GET, PUT, DELETE, POST, OPTIONS
allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,x-grpc-test-echo-initial,x-grpc-test-echo-trailing-bin,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
max_age: "1728000"
expose_headers: x-grpc-test-echo-initial,x-grpc-test-echo-trailing-bin,grpc-status,grpc-message
http_filters:
- name: envoy.grpc_web
- name: envoy.cors
- name: envoy.router
clusters:
- name: interop_service
connect_timeout: 0.25s
type: logical_dns
http2_protocol_options: {}
lb_policy: round_robin
hosts: [{ socket_address: { address: localhost, port_value: 7074 }}]

31
test/interop/index.html Normal file
View File

@ -0,0 +1,31 @@
<!-- 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. -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Interop Test</title>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<script src="//ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="./dist/main.js"></script>
</head>
<body>
<div class="container">
<div class="row" id="first">
<p>Please open up the console to see the test results.</p>
</div>
</div>
</body>
</html>

15
test/interop/package.json Normal file
View File

@ -0,0 +1,15 @@
{
"name": "grpc-web-interop-test",
"version": "0.1.0",
"description": "gRPC-Web Interop Test Client",
"license": "Apache-2.0",
"dependencies": {
"google-protobuf": "^3.6.1",
"grpc-web": "^1.0.0"
},
"devDependencies": {
"browserify": "^16.2.2",
"webpack": "^4.16.5",
"webpack-cli": "^3.1.0"
}
}

View File

@ -0,0 +1,4 @@
module.exports = {
mode: "production",
entry: "./client.js",
};