chore: Reorganize examples and interop crates (#180)

* chore: Reorganize examples and interop crates

* fix interop tests
This commit is contained in:
Lucio Franco
2019-12-12 11:53:15 -05:00
committed by GitHub
parent f096a238ac
commit d9a481baef
69 changed files with 25 additions and 22 deletions
+39
View File
@@ -0,0 +1,39 @@
[package]
name = "tonic-interop"
version = "0.1.0"
authors = ["Lucio Franco <[email protected]>"]
edition = "2018"
publish = false
license = "MIT"
[[bin]]
name = "client"
path = "src/bin/client.rs"
[[bin]]
name = "server"
path = "src/bin/server.rs"
[dependencies]
tokio = { version = "0.2", features = ["rt-threaded", "time", "macros", "stream", "fs"] }
tonic = { path = "../tonic", features = ["tls"] }
prost = "0.5"
prost-derive = "0.5"
bytes = "0.4"
http = "0.2"
futures-core = "0.3"
futures-util = "0.3"
async-stream = "0.2"
# tower = "=0.3.0-alpha.2"
tower = { git = "https://github.com/tower-rs/tower" }
http-body = "0.3"
console = "0.9"
structopt = "0.2"
tracing = "0.1"
tracing-subscriber = "0.2.0-alpha"
tracing-log = "0.1.0"
[build-dependencies]
tonic-build = { path = "../tonic-build" }
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
+8
View File
@@ -0,0 +1,8 @@
fn main() {
let proto = "proto/grpc/testing/test.proto";
tonic_build::compile_protos(proto).unwrap();
// prevent needing to rebuild if files (or deps) haven't changed
println!("cargo:rerun-if-changed={}", proto);
}
+17
View File
@@ -0,0 +1,17 @@
# Tonic Testing Certificates
This directory contains certificates used for testing interop between Tonic's
implementation of gRPC and the Go implementation. Certificates are generated
using [`terraform`][tf].
To regenerate certificates for some reason, do the following:
1. Install Terraform 0.12 (or higher)
1. From the `cert-generator` directory, run:
1. `terraform init`
1. `terraform apply`
This will generate certificates and write them to the filesystem. The effective
version should be committed to git.
[tf]: https://terraform.io
+20
View File
@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDRzCCAi+gAwIBAgIRAO7dzPqhReVW2U6D1V1DTYAwDQYJKoZIhvcNAQELBQAw
PTEOMAwGA1UEChMFVG9raW8xEDAOBgNVBAsTB1Rlc3RpbmcxGTAXBgNVBAMTEFRv
bmljIFRlc3RpbmcgQ0EwHhcNMTkxMTA5MTY0NzU0WhcNMjkxMTA2MTY0NzU0WjA9
MQ4wDAYDVQQKEwVUb2tpbzEQMA4GA1UECxMHVGVzdGluZzEZMBcGA1UEAxMQVG9u
aWMgVGVzdGluZyBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM+A
e+Y3wDdOm7yUi6xHINw1QE0k1D51U+DAMOLcUq17FYh3fp+OOaqq4znnEx8WkKVl
FuoW4xzIrv2ywn0hFADCxaVpjMuCxj313D7LMZExa98TuFF3Jg2GYScBRQKjfyRv
CV+cSHAvzEstd5ckdiz985Zqnepiy7R9k2CstO45ULG4UoVha+VgmYJ5qXqBXbso
LbDXzrjjQFSmbw1yh9lQvzsk0UyU5Hvi0Otka4LZJsNaNFWkltl8v37QWG6sH8+f
Ur7fELIDmbScGSiqvEm0EZLZHqLU669NvUoFtTKNfKghT83DW9kErzdg5EcVFQom
Ov8cIW9MkBwDJXe6D88CAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgIEMA8GA1UdEwEB
/wQFMAMBAf8wHQYDVR0OBBYEFE5N36NK1qLWGDxSJP3IF9H2oLO/MA0GCSqGSIb3
DQEBCwUAA4IBAQAm27Wdq6+0IxvattJW0j7lnh9AC7aMQEbUQPYM8iO6WVhXshaN
d7YbLDGwRtATzLeA/0laovEL5pc/1NnMsqG4DspD3glXcASUiu5TrzPvBrAWByQn
8C9tGUbyP1yJMo42Mzs0hIDsaUsucynZcdxAErjJjKB9Ps3KeaA6LUr/igN0649S
qQ6bHZdNEIhjyCY7TDJArjPTgdSYjx13XkVbZ1cN/ssZI7Ag8WByhVEHeoE13fqw
ue5oTNat0qNFP0Yo8XB0whPhN2wbCbHoc5khXJiBrMEjAAgJUiWRy3ITG88SIak+
1dtqgxS2T0nmdRlXBYNNhYKdgWYJq1PwDp9a
-----END CERTIFICATE-----
+3
View File
@@ -0,0 +1,3 @@
.terraform/
*.tfstate
*.tfstate.backup
+27
View File
@@ -0,0 +1,27 @@
resource "tls_private_key" "root" {
algorithm = "RSA"
rsa_bits = "2048"
}
resource "tls_self_signed_cert" "root" {
key_algorithm = tls_private_key.root.algorithm
private_key_pem = tls_private_key.root.private_key_pem
validity_period_hours = 87600
early_renewal_hours = 8760
is_ca_certificate = true
allowed_uses = ["cert_signing"]
subject {
common_name = "Tonic Testing CA"
organization = "Tokio"
organizational_unit = "Testing"
}
}
resource "local_file" "ca_cert" {
filename = "../ca.pem"
content = tls_self_signed_cert.root.cert_pem
}
@@ -0,0 +1,40 @@
resource "tls_private_key" "server" {
algorithm = "RSA"
rsa_bits = "2048"
}
resource "tls_cert_request" "server" {
key_algorithm = tls_private_key.server.algorithm
private_key_pem = tls_private_key.server.private_key_pem
subject {
common_name = "Tonic Test Server Cert"
}
dns_names = [
"*.test.google.fr",
]
}
resource "tls_locally_signed_cert" "server" {
cert_request_pem = tls_cert_request.server.cert_request_pem
ca_key_algorithm = tls_private_key.root.algorithm
ca_private_key_pem = tls_private_key.root.private_key_pem
ca_cert_pem = tls_self_signed_cert.root.cert_pem
validity_period_hours = 43800
early_renewal_hours = 8760
allowed_uses = ["server_auth"]
}
resource "local_file" "server_cert" {
filename = "../server1.pem"
content = tls_locally_signed_cert.server.cert_pem
}
resource "local_file" "server_key" {
filename = "../server1.key"
content = tls_private_key.server.private_key_pem
}
+27
View File
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAvSo8ttodDg4wwjwSPURcTgVPh7Iv9E+PsCTHW7b1Dov/tjxs
nhgLZkySicSLIhHv+/17DYHaMd7alx5AYb+Bxk/KvSuR/sqg0QJcuQ4prpzccQHY
mqJTeMWFCOvAUSSOZXwOxKHFNoLCVQsZMMs1JmUwAoun9ocyuoXuNFa/2GmNO+cN
tBSa1ZnEyhuW9R8lFdp41e9OG1c6dgdh3Gq6NEqDaRN/FbaBAQJO6u8jDClcqeB5
uPfXKEENk8ed71zRRJ7u4Pjn+qCgy9daz6hRuP7MGKnrXt1j5f73JnZF5bFuM+Bg
6MqG6XVvrIVDdvod2qWpMTjCpINp45i0lkZpAQIDAQABAoIBAQCu50LD/uAmgtBq
h4iFxZNjQF3MpeDZEEdXImqCTqQ/EwsYwL3dX3YK3HoRj/zlP5iZckI4tvu8aMXM
PFhjCONBLb3TM1oGL+yJ1JlPMd0wajEY/A/+ymBLprXfDbwASsCu7QnqnXjvce+l
GmHsT7eRDLZbZC2lMFSjSfp5wkwYF8lBfg2tDswgBX2mT4lZ/nUIjpr1x1y34BQL
yPYHSsSnoTSC6dqWkpuiQyyTraeknCsE4lw03C2XWcpCFaNt6ZrzAmSeX7/MK+Xp
blQ4bQmeydCwfsXFBdE/lOWUIjc8mkBN0c9wbiBJwyAH09QzUVcXlgjH7IBwKSMc
mFpLvt3BAoGBAOGNHc1URPiSJaIc86nkWvmNiISxqHUVOrZPwOLJMciJz75XSmVs
QlydXIepunU5WZJJiIV2fL6FyZ7RnCGLCvFEymewobexAjL3ffN4oN5s2g2e8fs+
vhu3xLxo3fmhcRLbxub0qOJUu//2lcBYRXHVu+lb4qvisnn4BDMxbAFNAoGBANaz
o5Zo6uL5WHcNjwKa4YtWA4/pzAzsXwNqPPUyWL9uvAP3YS7ikQvzLJXDUTCtopZ5
xTvuShRPfvrIUkvK9XOok3PNeyPeBoRQ8W9sP6n/5gSULy7sHALJxZscMf7GD81+
yfH+R+67vwW1etwP3LHuC1NirCcpvLZfWyfUcqyFAoGBAKeqzXKrqDHYAp3GQ+QR
WweUDN4HayDOTTzlgI+V3KokuAfYv/cxSQur9vLqWy91GH7EpvX/pK/EqKKlUxkk
UVgVORlnlnAE54uXq0toar2t0VK6y0tn0s6sB1W/5vMA7huEwRFC4qCNOMwIND4t
4EHFDtFketYnyWEd25Fqtc0pAoGATpvJClnxnhbDMBuzv7VrXPOqLDfisNyeUQbF
uNStL7HgfudFGsBzcNeg/Fhd0p/QRp3g+/dcAiG1ESblEsEFq0oOarjSHCi/ZBSq
wSv2B00dL5H90IU8ID0173ucRnbH9Go2kDaUqbDt2K5AhG/+UtsgJHCdLV2XrYIu
QuAC+G0CgYBW2O4FjDLG16kivtEziCW+Z/iLeUM3r8lDzf8iz1Dg+72AnlmAFSzz
QaY2GxdatZHBCMeZ0eaMxxFyuT2IPKOiyIHQ6diJBX+CFNHxgPfCAQ8DTVi6aJtS
NiY/yUYTsQbh6Mey8QAC5wdoyuVUKQO2SwyNiHFWP+i9aFXr/rv98w==
-----END RSA PRIVATE KEY-----
+20
View File
@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDSzCCAjOgAwIBAgIQQY5jNBnC4CbgToZB9CzLYzANBgkqhkiG9w0BAQsFADA9
MQ4wDAYDVQQKEwVUb2tpbzEQMA4GA1UECxMHVGVzdGluZzEZMBcGA1UEAxMQVG9u
aWMgVGVzdGluZyBDQTAeFw0xOTExMDkxNjQ3NTRaFw0yNDExMDcxNjQ3NTRaMCEx
HzAdBgNVBAMTFlRvbmljIFRlc3QgU2VydmVyIENlcnQwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQC9Kjy22h0ODjDCPBI9RFxOBU+Hsi/0T4+wJMdbtvUO
i/+2PGyeGAtmTJKJxIsiEe/7/XsNgdox3tqXHkBhv4HGT8q9K5H+yqDRAly5Dimu
nNxxAdiaolN4xYUI68BRJI5lfA7EocU2gsJVCxkwyzUmZTACi6f2hzK6he40Vr/Y
aY075w20FJrVmcTKG5b1HyUV2njV704bVzp2B2Hcaro0SoNpE38VtoEBAk7q7yMM
KVyp4Hm499coQQ2Tx53vXNFEnu7g+Of6oKDL11rPqFG4/swYqete3WPl/vcmdkXl
sW4z4GDoyobpdW+shUN2+h3apakxOMKkg2njmLSWRmkBAgMBAAGjYzBhMBMGA1Ud
JQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUTk3fo0rW
otYYPFIk/cgX0fags78wGwYDVR0RBBQwEoIQKi50ZXN0Lmdvb2dsZS5mcjANBgkq
hkiG9w0BAQsFAAOCAQEATypihaP3RfHar9DZGeKwPz25BQBhGJE2lEwlehUuNBkG
M1+yQw0p3Yj2n5tR87Qp+G5rDN51e2OCWbEMTo0iAiGdic4N4FXnmA7R9QDcBcKw
YmKIXd48F+Ceh5XYGFuR14dTshu2Zajwcw4OW3dhvEbv9h5pMCJXplhAPHvhTKTM
TTl0fjDkBcOdKWswdQCtt2xOqcQhpYdVYClYym22WYDcMDr7CqiC/y3jLl6eVpsA
hPDAVZqpZSpnV6isihoUyDeSVT830/E/n8e756l7gBNxpsYF+PqDBVPiAU7Gjp99
EegYn7LiB+s4tgHeSMdNclffWQJ3PameoaLHdikGLA==
-----END CERTIFICATE-----
+27
View File
@@ -0,0 +1,27 @@
// 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 {}
+168
View File
@@ -0,0 +1,168 @@
// 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;
}
// DEPRECATED, don't use. To be removed shortly.
// 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 {
// DEPRECATED, don't use. To be removed shortly.
// 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;
}
// Unary request.
message SimpleRequest {
// DEPRECATED, don't use. To be removed shortly.
// 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;
}
// 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;
}
// 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 {
// DEPRECATED, don't use. To be removed shortly.
// 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;
}
+78
View File
@@ -0,0 +1,78 @@
// 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 "empty.proto";
import "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);
}
+130
View File
@@ -0,0 +1,130 @@
use std::time::Duration;
use structopt::{clap::arg_enum, StructOpt};
use tonic::transport::Endpoint;
use tonic::transport::{Certificate, ClientTlsConfig};
use tonic_interop::client;
#[derive(StructOpt)]
struct Opts {
#[structopt(
long = "test_case",
use_delimiter = true,
min_values = 1,
raw(possible_values = r#"&Testcase::variants()"#)
)]
test_case: Vec<Testcase>,
#[structopt(long)]
use_tls: bool,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_interop::trace_init();
let matches = Opts::from_args();
let test_cases = matches.test_case;
#[allow(unused_mut)]
let mut endpoint = Endpoint::from_static("http://localhost:10000")
.timeout(Duration::from_secs(5))
.concurrency_limit(30);
if matches.use_tls {
let pem = tokio::fs::read("interop/data/ca.pem").await?;
let ca = Certificate::from_pem(pem);
endpoint = endpoint.tls_config(
ClientTlsConfig::with_rustls()
.ca_certificate(ca)
.domain_name("foo.test.google.fr"),
);
}
let channel = endpoint.connect().await?;
let mut client = client::TestClient::new(channel.clone());
let mut unimplemented_client = client::UnimplementedClient::new(channel);
let mut failures = Vec::new();
for test_case in test_cases {
println!("{:?}:", test_case);
let mut test_results = Vec::new();
match test_case {
Testcase::empty_unary => client::empty_unary(&mut client, &mut test_results).await,
Testcase::large_unary => client::large_unary(&mut client, &mut test_results).await,
Testcase::client_streaming => {
client::client_streaming(&mut client, &mut test_results).await
}
Testcase::server_streaming => {
client::server_streaming(&mut client, &mut test_results).await
}
Testcase::ping_pong => client::ping_pong(&mut client, &mut test_results).await,
Testcase::empty_stream => client::empty_stream(&mut client, &mut test_results).await,
Testcase::status_code_and_message => {
client::status_code_and_message(&mut client, &mut test_results).await
}
Testcase::special_status_message => {
client::special_status_message(&mut client, &mut test_results).await
}
Testcase::unimplemented_method => {
client::unimplemented_method(&mut client, &mut test_results).await
}
Testcase::unimplemented_service => {
client::unimplemented_service(&mut unimplemented_client, &mut test_results).await
}
Testcase::custom_metadata => {
client::custom_metadata(&mut client, &mut test_results).await
}
_ => unimplemented!(),
}
for result in test_results {
println!(" {}", result);
if result.is_failed() {
failures.push(result);
}
}
}
if !failures.is_empty() {
println!("{} tests failed", failures.len());
std::process::exit(1);
}
Ok(())
}
arg_enum! {
#[derive(Debug, Copy, Clone)]
#[allow(non_camel_case_types)]
enum Testcase {
empty_unary,
cacheable_unary,
large_unary,
client_compressed_unary,
server_compressed_unary,
client_streaming,
client_compressed_streaming,
server_streaming,
server_compressed_streaming,
ping_pong,
empty_stream,
compute_engine_creds,
jwt_token_creds,
oauth2_auth_token,
per_rpc_creds,
custom_metadata,
status_code_and_message,
special_status_message,
unimplemented_method,
unimplemented_service,
cancel_after_begin,
cancel_after_first_response,
timeout_on_sleeping_server,
concurrent_large_unary
}
}
+70
View File
@@ -0,0 +1,70 @@
use http::header::HeaderName;
use structopt::StructOpt;
use tonic::body::BoxBody;
use tonic::client::GrpcService;
use tonic::transport::Server;
use tonic::transport::{Identity, ServerTlsConfig};
use tonic_interop::{server, MergeTrailers};
#[derive(StructOpt)]
struct Opts {
#[structopt(long)]
use_tls: bool,
}
#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
tonic_interop::trace_init();
let matches = Opts::from_args();
let addr = "127.0.0.1:10000".parse().unwrap();
let mut builder = Server::builder().interceptor_fn(|svc, req| {
let echo_header = req
.headers()
.get("x-grpc-test-echo-initial")
.map(Clone::clone);
let echo_trailer = req
.headers()
.get("x-grpc-test-echo-trailing-bin")
.map(Clone::clone)
.map(|v| (HeaderName::from_static("x-grpc-test-echo-trailing-bin"), v));
let call = svc.call(req);
async move {
let mut res = call.await?;
if let Some(echo_header) = echo_header {
res.headers_mut()
.insert("x-grpc-test-echo-initial", echo_header);
}
Ok(res
.map(|b| MergeTrailers::new(b, echo_trailer))
.map(BoxBody::new))
}
});
if matches.use_tls {
let cert = tokio::fs::read("interop/data/server1.pem").await?;
let key = tokio::fs::read("interop/data/server1.key").await?;
let identity = Identity::from_pem(cert, key);
builder = builder.tls_config(ServerTlsConfig::with_rustls().identity(identity));
}
let test_service = server::TestServiceServer::new(server::TestService::default());
let unimplemented_service =
server::UnimplementedServiceServer::new(server::UnimplementedService::default());
builder
.add_service(test_service)
.add_service(unimplemented_service)
.serve(addr)
.await?;
Ok(())
}
+409
View File
@@ -0,0 +1,409 @@
use crate::{
pb::testservice_client::*, pb::unimplementedservice_client::*, pb::*, test_assert,
TestAssertion,
};
use futures_util::{future, stream, StreamExt};
use tokio::sync::mpsc;
use tonic::transport::Channel;
use tonic::{metadata::MetadataValue, Code, Request, Response, Status};
pub type TestClient = TestServiceClient<Channel>;
pub type UnimplementedClient = UnimplementedServiceClient<Channel>;
const LARGE_REQ_SIZE: usize = 271828;
const LARGE_RSP_SIZE: i32 = 314159;
const REQUEST_LENGTHS: &'static [i32] = &[27182, 8, 1828, 45904];
const RESPONSE_LENGTHS: &'static [i32] = &[31415, 9, 2653, 58979];
const TEST_STATUS_MESSAGE: &'static str = "test status message";
const SPECIAL_TEST_STATUS_MESSAGE: &'static str =
"\t\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\t\n";
pub async fn empty_unary(client: &mut TestClient, assertions: &mut Vec<TestAssertion>) {
let result = client.empty_call(Request::new(Empty {})).await;
assertions.push(test_assert!(
"call must be successful",
result.is_ok(),
format!("result={:?}", result)
));
if let Ok(response) = result {
let body = response.into_inner();
assertions.push(test_assert!(
"body must not be null",
body == Empty {},
format!("body={:?}", body)
));
}
}
pub async fn large_unary(client: &mut TestClient, assertions: &mut Vec<TestAssertion>) {
use std::mem;
let payload = crate::client_payload(LARGE_REQ_SIZE);
let req = SimpleRequest {
response_type: PayloadType::Compressable as i32,
response_size: LARGE_RSP_SIZE,
payload: Some(payload),
..Default::default()
};
let result = client.unary_call(Request::new(req)).await;
assertions.push(test_assert!(
"call must be successful",
result.is_ok(),
format!("result={:?}", result)
));
if let Ok(response) = result {
let body = response.into_inner();
let payload_len = body.payload.as_ref().map(|p| p.body.len()).unwrap_or(0);
assertions.push(test_assert!(
"body must be 314159 bytes",
payload_len == LARGE_RSP_SIZE as usize,
format!("mem::size_of_val(&body)={:?}", mem::size_of_val(&body))
));
}
}
// pub async fn cachable_unary(client: &mut Client, assertions: &mut Vec<TestAssertion>) {
// let payload = Payload {
// r#type: PayloadType::Compressable as i32,
// body: format!("{:?}", std::time::Instant::now()).into_bytes(),
// };
// let req = SimpleRequest {
// response_type: PayloadType::Compressable as i32,
// payload: Some(payload),
// ..Default::default()
// };
// client.
// }
pub async fn client_streaming(client: &mut TestClient, assertions: &mut Vec<TestAssertion>) {
let requests = REQUEST_LENGTHS.iter().map(|len| StreamingInputCallRequest {
payload: Some(crate::client_payload(*len as usize)),
..Default::default()
});
let stream = stream::iter(requests);
let result = client.streaming_input_call(Request::new(stream)).await;
assertions.push(test_assert!(
"call must be successful",
result.is_ok(),
format!("result={:?}", result)
));
if let Ok(response) = result {
let body = response.into_inner();
assertions.push(test_assert!(
"aggregated payload size must be 74922 bytes",
body.aggregated_payload_size == 74922,
format!("aggregated_payload_size={:?}", body.aggregated_payload_size)
));
}
}
pub async fn server_streaming(client: &mut TestClient, assertions: &mut Vec<TestAssertion>) {
let req = StreamingOutputCallRequest {
response_parameters: RESPONSE_LENGTHS
.iter()
.map(|len| ResponseParameters::with_size(*len))
.collect(),
..Default::default()
};
let req = Request::new(req);
let result = client.streaming_output_call(req).await;
assertions.push(test_assert!(
"call must be successful",
result.is_ok(),
format!("result={:?}", result)
));
if let Ok(response) = result {
let responses = response
.into_inner()
.filter_map(|m| future::ready(m.ok()))
.collect::<Vec<_>>()
.await;
let actual_response_lengths = crate::response_lengths(&responses);
let asserts = vec![
test_assert!(
"there should be four responses",
responses.len() == 4,
format!("responses.len()={:?}", responses.len())
),
test_assert!(
"the response payload sizes should match input",
RESPONSE_LENGTHS == actual_response_lengths.as_slice(),
format!("{:?}={:?}", RESPONSE_LENGTHS, actual_response_lengths)
),
];
assertions.extend(asserts);
}
}
pub async fn ping_pong(client: &mut TestClient, assertions: &mut Vec<TestAssertion>) {
let (tx, rx) = mpsc::unbounded_channel();
tx.send(make_ping_pong_request(0)).unwrap();
let result = client.full_duplex_call(Request::new(rx)).await;
assertions.push(test_assert!(
"call must be successful",
result.is_ok(),
format!("result={:?}", result)
));
if let Ok(mut response) = result.map(Response::into_inner) {
let mut responses = Vec::new();
loop {
match response.next().await {
Some(result) => {
responses.push(result.unwrap());
if responses.len() == REQUEST_LENGTHS.len() {
drop(tx);
break;
} else {
tx.send(make_ping_pong_request(responses.len())).unwrap();
}
}
None => {
assertions.push(TestAssertion::Failed {
description:
"server should keep the stream open until the client closes it",
expression: "Stream terminated unexpectedly early",
why: None,
});
break;
}
}
}
let actual_response_lengths = crate::response_lengths(&responses);
assertions.push(test_assert!(
"there should be four responses",
responses.len() == RESPONSE_LENGTHS.len(),
format!("{:?}={:?}", responses.len(), RESPONSE_LENGTHS.len())
));
assertions.push(test_assert!(
"the response payload sizes should match input",
RESPONSE_LENGTHS == actual_response_lengths.as_slice(),
format!("{:?}={:?}", RESPONSE_LENGTHS, actual_response_lengths)
));
}
}
pub async fn empty_stream(client: &mut TestClient, assertions: &mut Vec<TestAssertion>) {
let stream = stream::iter(Vec::new());
let result = client.full_duplex_call(Request::new(stream)).await;
assertions.push(test_assert!(
"call must be successful",
result.is_ok(),
format!("result={:?}", result)
));
if let Ok(response) = result.map(Response::into_inner) {
let responses = response.collect::<Vec<_>>().await;
assertions.push(test_assert!(
"there should be no responses",
responses.len() == 0,
format!("responses.len()={:?}", responses.len())
));
}
}
pub async fn status_code_and_message(client: &mut TestClient, assertions: &mut Vec<TestAssertion>) {
fn validate_response<T>(result: Result<T, Status>, assertions: &mut Vec<TestAssertion>)
where
T: std::fmt::Debug,
{
assertions.push(test_assert!(
"call must fail with unknown status code",
match &result {
Err(status) => status.code() == Code::Unknown,
_ => false,
},
format!("result={:?}", result)
));
assertions.push(test_assert!(
"call must respsond with expected status message",
match &result {
Err(status) => status.message() == TEST_STATUS_MESSAGE,
_ => false,
},
format!("result={:?}", result)
));
}
let simple_req = SimpleRequest {
response_status: Some(EchoStatus {
code: 2,
message: TEST_STATUS_MESSAGE.to_string(),
..Default::default()
}),
..Default::default()
};
let duplex_req = StreamingOutputCallRequest {
response_status: Some(EchoStatus {
code: 2,
message: TEST_STATUS_MESSAGE.to_string(),
..Default::default()
}),
..Default::default()
};
let result = client.unary_call(Request::new(simple_req)).await;
validate_response(result, assertions);
let stream = stream::iter(vec![duplex_req]);
let result = match client.full_duplex_call(Request::new(stream)).await {
Ok(response) => {
let stream = response.into_inner();
let responses = stream.collect::<Vec<_>>().await;
Ok(responses)
}
Err(e) => Err(e),
};
validate_response(result, assertions);
}
pub async fn special_status_message(client: &mut TestClient, assertions: &mut Vec<TestAssertion>) {
let req = SimpleRequest {
response_status: Some(EchoStatus {
code: 2,
message: SPECIAL_TEST_STATUS_MESSAGE.to_string(),
..Default::default()
}),
..Default::default()
};
let result = client.unary_call(Request::new(req)).await;
assertions.push(test_assert!(
"call must fail with unknown status code",
match &result {
Err(status) => status.code() == Code::Unknown,
_ => false,
},
format!("result={:?}", result)
));
assertions.push(test_assert!(
"call must respsond with expected status message",
match &result {
Err(status) => status.message() == SPECIAL_TEST_STATUS_MESSAGE,
_ => false,
},
format!("result={:?}", result)
));
}
pub async fn unimplemented_method(client: &mut TestClient, assertions: &mut Vec<TestAssertion>) {
let result = client.unimplemented_call(Request::new(Empty {})).await;
assertions.push(test_assert!(
"call must fail with unimplemented status code",
match &result {
Err(status) => status.code() == Code::Unimplemented,
_ => false,
},
format!("result={:?}", result)
));
}
pub async fn unimplemented_service(
client: &mut UnimplementedClient,
assertions: &mut Vec<TestAssertion>,
) {
let result = client.unimplemented_call(Request::new(Empty {})).await;
assertions.push(test_assert!(
"call must fail with unimplemented status code",
match &result {
Err(status) => status.code() == Code::Unimplemented,
_ => false,
},
format!("result={:?}", result)
));
}
pub async fn custom_metadata(client: &mut TestClient, assertions: &mut Vec<TestAssertion>) {
let key1 = "x-grpc-test-echo-initial";
let value1 = MetadataValue::from_str("test_initial_metadata_value").unwrap();
let key2 = "x-grpc-test-echo-trailing-bin";
let value2 = MetadataValue::from_bytes(&[0xab, 0xab, 0xab]);
let req = SimpleRequest {
response_type: PayloadType::Compressable as i32,
response_size: LARGE_RSP_SIZE,
payload: Some(crate::client_payload(LARGE_REQ_SIZE)),
..Default::default()
};
let mut req_unary = Request::new(req);
req_unary.metadata_mut().insert(key1, value1.clone());
req_unary.metadata_mut().insert_bin(key2, value2.clone());
let stream = stream::iter(vec![make_ping_pong_request(0)]);
let mut req_stream = Request::new(stream);
req_stream.metadata_mut().insert(key1, value1.clone());
req_stream.metadata_mut().insert_bin(key2, value2.clone());
let response = client
.unary_call(req_unary)
.await
.expect("call should pass.");
assertions.push(test_assert!(
"metadata string must match in unary",
response.metadata().get(key1) == Some(&value1),
format!("result={:?}", response.metadata().get(key1))
));
assertions.push(test_assert!(
"metadata bin must match in unary",
response.metadata().get_bin(key2) == Some(&value2),
format!("result={:?}", response.metadata().get_bin(key1))
));
let response = client
.full_duplex_call(req_stream)
.await
.expect("call should pass.");
assertions.push(test_assert!(
"metadata string must match in unary",
response.metadata().get(key1) == Some(&value1),
format!("result={:?}", response.metadata().get(key1))
));
let mut stream = response.into_inner();
let trailers = stream.trailers().await.unwrap().unwrap();
assertions.push(test_assert!(
"metadata bin must match in unary",
trailers.get_bin(key2) == Some(&value2),
format!("result={:?}", trailers.get_bin(key1))
));
}
fn make_ping_pong_request(idx: usize) -> StreamingOutputCallRequest {
let req_len = REQUEST_LENGTHS[idx];
let resp_len = RESPONSE_LENGTHS[idx];
StreamingOutputCallRequest {
response_parameters: vec![ResponseParameters::with_size(resp_len)],
payload: Some(crate::client_payload(req_len as usize)),
..Default::default()
}
}
+187
View File
@@ -0,0 +1,187 @@
#![recursion_limit = "256"]
pub mod client;
pub mod server;
pub mod pb {
#![allow(dead_code)]
#![allow(unused_imports)]
include!(concat!(env!("OUT_DIR"), "/grpc.testing.rs"));
}
use http::header::{HeaderMap, HeaderName, HeaderValue};
use http_body::Body;
use std::{
default, fmt, iter,
pin::Pin,
task::{Context, Poll},
};
pub fn trace_init() {
let sub = tracing_subscriber::FmtSubscriber::builder()
.with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env())
.finish();
let _ = tracing::subscriber::set_global_default(sub);
let _ = tracing_log::LogTracer::init();
}
pub fn client_payload(size: usize) -> pb::Payload {
pb::Payload {
r#type: default::Default::default(),
body: iter::repeat(0u8).take(size).collect(),
}
}
pub fn server_payload(size: usize) -> pb::Payload {
pb::Payload {
r#type: default::Default::default(),
body: iter::repeat(0u8).take(size).collect(),
}
}
impl pb::ResponseParameters {
fn with_size(size: i32) -> Self {
pb::ResponseParameters {
size,
..Default::default()
}
}
}
fn response_length(response: &pb::StreamingOutputCallResponse) -> i32 {
match &response.payload {
Some(ref payload) => payload.body.len() as i32,
None => 0,
}
}
fn response_lengths(responses: &Vec<pb::StreamingOutputCallResponse>) -> Vec<i32> {
responses.iter().map(&response_length).collect()
}
#[derive(Debug)]
pub enum TestAssertion {
Passed {
description: &'static str,
},
Failed {
description: &'static str,
expression: &'static str,
why: Option<String>,
},
}
impl TestAssertion {
pub fn is_failed(&self) -> bool {
match self {
TestAssertion::Failed { .. } => true,
_ => false,
}
}
}
impl fmt::Display for TestAssertion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use console::{style, Emoji};
match *self {
TestAssertion::Passed { ref description } => write!(
f,
"{check} {desc}",
check = style(Emoji("", "+")).green(),
desc = style(description).green(),
),
TestAssertion::Failed {
ref description,
ref expression,
why: Some(ref why),
} => write!(
f,
"{check} {desc}\n in `{exp}`: {why}",
check = style(Emoji("", "x")).red(),
desc = style(description).red(),
exp = style(expression).red(),
why = style(why).red(),
),
TestAssertion::Failed {
ref description,
ref expression,
why: None,
} => write!(
f,
"{check} {desc}\n in `{exp}`",
check = style(Emoji("", "x")).red(),
desc = style(description).red(),
exp = style(expression).red(),
),
}
}
}
#[macro_export]
macro_rules! test_assert {
($description:expr, $assertion:expr) => {
if $assertion {
crate::TestAssertion::Passed {
description: $description,
}
} else {
TestAssertion::Failed {
description: $description,
expression: stringify!($assertion),
why: None,
}
}
};
($description:expr, $assertion:expr, $why:expr) => {
if $assertion {
crate::TestAssertion::Passed {
description: $description,
}
} else {
crate::TestAssertion::Failed {
description: $description,
expression: stringify!($assertion),
why: Some($why),
}
}
};
}
pub struct MergeTrailers<B> {
inner: B,
trailer: Option<(HeaderName, HeaderValue)>,
}
impl<B> MergeTrailers<B> {
pub fn new(inner: B, trailer: Option<(HeaderName, HeaderValue)>) -> Self {
Self { inner, trailer }
}
}
impl<B: Body + Unpin> Body for MergeTrailers<B> {
type Data = B::Data;
type Error = B::Error;
fn poll_data(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Self::Data, Self::Error>>> {
Pin::new(&mut self.inner).poll_data(cx)
}
fn poll_trailers(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<Option<HeaderMap>, Self::Error>> {
Pin::new(&mut self.inner).poll_trailers(cx).map_ok(|h| {
h.map(|mut headers| {
if let Some((key, value)) = &self.trailer {
headers.insert(key.clone(), value.clone());
}
headers
})
})
}
}
+161
View File
@@ -0,0 +1,161 @@
use crate::pb::{self, *};
use async_stream::try_stream;
use futures_util::{stream, StreamExt, TryStreamExt};
use std::pin::Pin;
use std::time::Duration;
use tonic::{Code, Request, Response, Status};
pub use pb::testservice_server::TestServiceServer;
pub use pb::unimplementedservice_server::UnimplementedServiceServer;
#[derive(Default, Clone)]
pub struct TestService;
type Result<T> = std::result::Result<Response<T>, Status>;
type Streaming<T> = Request<tonic::Streaming<T>>;
type Stream<T> = Pin<
Box<dyn futures_core::Stream<Item = std::result::Result<T, Status>> + Send + Sync + 'static>,
>;
#[tonic::async_trait]
impl pb::testservice_server::TestService for TestService {
async fn empty_call(&self, _request: Request<Empty>) -> Result<Empty> {
Ok(Response::new(Empty {}))
}
async fn unary_call(&self, request: Request<SimpleRequest>) -> Result<SimpleResponse> {
let req = request.into_inner();
if let Some(echo_status) = req.response_status {
let status = Status::new(Code::from_i32(echo_status.code), echo_status.message);
return Err(status);
}
let res_size = if req.response_size >= 0 {
req.response_size as usize
} else {
let status = Status::new(Code::InvalidArgument, "response_size cannot be negative");
return Err(status);
};
let res = SimpleResponse {
payload: Some(Payload {
body: vec![0; res_size],
..Default::default()
}),
..Default::default()
};
Ok(Response::new(res))
}
async fn cacheable_unary_call(&self, _: Request<SimpleRequest>) -> Result<SimpleResponse> {
unimplemented!()
}
type StreamingOutputCallStream = Stream<StreamingOutputCallResponse>;
async fn streaming_output_call(
&self,
req: Request<StreamingOutputCallRequest>,
) -> Result<Self::StreamingOutputCallStream> {
let StreamingOutputCallRequest {
response_parameters,
..
} = req.into_inner();
let stream = try_stream! {
for param in response_parameters {
tokio::time::delay_for(Duration::from_micros(param.interval_us as u64)).await;
let payload = crate::server_payload(param.size as usize);
yield StreamingOutputCallResponse { payload: Some(payload) };
}
};
Ok(Response::new(
Box::pin(stream) as Self::StreamingOutputCallStream
))
}
async fn streaming_input_call(
&self,
req: Streaming<StreamingInputCallRequest>,
) -> Result<StreamingInputCallResponse> {
let mut stream = req.into_inner();
let mut aggregated_payload_size = 0 as i32;
while let Some(msg) = stream.try_next().await? {
aggregated_payload_size += msg.payload.unwrap().body.len() as i32;
}
let res = StreamingInputCallResponse {
aggregated_payload_size,
};
Ok(Response::new(res))
}
type FullDuplexCallStream = Stream<StreamingOutputCallResponse>;
async fn full_duplex_call(
&self,
req: Streaming<StreamingOutputCallRequest>,
) -> Result<Self::FullDuplexCallStream> {
let mut stream = req.into_inner();
if let Some(first_msg) = stream.message().await? {
if let Some(echo_status) = first_msg.response_status {
let status = Status::new(Code::from_i32(echo_status.code), echo_status.message);
return Err(status);
}
let single_message = stream::iter(vec![Ok(first_msg)]);
let mut stream = single_message.chain(stream);
let stream = try_stream! {
while let Some(msg) = stream.try_next().await? {
if let Some(echo_status) = msg.response_status {
let status = Status::new(Code::from_i32(echo_status.code), echo_status.message);
Err(status)?;
}
for param in msg.response_parameters {
tokio::time::delay_for(Duration::from_micros(param.interval_us as u64)).await;
let payload = crate::server_payload(param.size as usize);
yield StreamingOutputCallResponse { payload: Some(payload) };
}
}
};
Ok(Response::new(Box::pin(stream) as Self::FullDuplexCallStream))
} else {
let stream = stream::empty();
Ok(Response::new(Box::pin(stream) as Self::FullDuplexCallStream))
}
}
type HalfDuplexCallStream = Stream<StreamingOutputCallResponse>;
async fn half_duplex_call(
&self,
_: Streaming<StreamingOutputCallRequest>,
) -> Result<Self::HalfDuplexCallStream> {
Err(Status::unimplemented("TODO"))
}
async fn unimplemented_call(&self, _: Request<Empty>) -> Result<Empty> {
Err(Status::unimplemented(""))
}
}
#[derive(Default)]
pub struct UnimplementedService;
#[tonic::async_trait]
impl pb::unimplementedservice_server::UnimplementedService for UnimplementedService {
async fn unimplemented_call(&self, _req: Request<Empty>) -> Result<Empty> {
Err(Status::unimplemented(""))
}
}
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
set -eu
set -o pipefail
set -x
echo "Running for OS: ${OSTYPE}"
case "$OSTYPE" in
darwin*) OS="darwin"; EXT="" ;;
linux*) OS="linux"; EXT="" ;;
msys*) OS="windows"; EXT=".exe" ;;
*) exit 2 ;;
esac
ARG="${1:-""}"
(cd interop && cargo build --bins)
SERVER="interop/bin/server_${OS}_amd64${EXT}"
# TLS_CA="interop/data/ca.pem"
TLS_CRT="interop/data/server1.pem"
TLS_KEY="interop/data/server1.key"
# run the test server
./"${SERVER}" ${ARG} --tls_cert_file $TLS_CRT --tls_key_file $TLS_KEY &
SERVER_PID=$!
echo ":; started grpc-go test server."
# trap exits to make sure we kill the server process when the script exits,
# regardless of why (errors, SIGTERM, etc).
trap 'echo ":; killing test server"; kill ${SERVER_PID};' EXIT
sleep 1
./target/debug/client \
--test_case=empty_unary,large_unary,client_streaming,server_streaming,ping_pong,\
empty_stream,status_code_and_message,special_status_message,unimplemented_method,\
unimplemented_service,custom_metadata ${ARG}
echo ":; killing test server"; kill ${SERVER_PID};
# run the test server
./target/debug/server ${ARG} &
SERVER_PID=$!
echo ":; started tonic test server."
# trap exits to make sure we kill the server process when the script exits,
# regardless of why (errors, SIGTERM, etc).
trap 'echo ":; killing test server"; kill ${SERVER_PID};' EXIT
sleep 1
./target/debug/client \
--test_case=empty_unary,large_unary,client_streaming,server_streaming,ping_pong,\
empty_stream,status_code_and_message,special_status_message,unimplemented_method,\
unimplemented_service,custom_metadata ${ARG}