grpc 1.30.2-x86-linux
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of grpc might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/etc/roots.pem +4644 -0
- data/grpc_c.32.ruby +0 -0
- data/grpc_c.64.ruby +0 -0
- data/src/ruby/bin/math_client.rb +140 -0
- data/src/ruby/bin/math_pb.rb +34 -0
- data/src/ruby/bin/math_server.rb +191 -0
- data/src/ruby/bin/math_services_pb.rb +51 -0
- data/src/ruby/bin/noproto_client.rb +93 -0
- data/src/ruby/bin/noproto_server.rb +97 -0
- data/src/ruby/ext/grpc/ext-export.clang +1 -0
- data/src/ruby/ext/grpc/ext-export.gcc +6 -0
- data/src/ruby/ext/grpc/extconf.rb +107 -0
- data/src/ruby/ext/grpc/rb_byte_buffer.c +64 -0
- data/src/ruby/ext/grpc/rb_byte_buffer.h +35 -0
- data/src/ruby/ext/grpc/rb_call.c +1050 -0
- data/src/ruby/ext/grpc/rb_call.h +53 -0
- data/src/ruby/ext/grpc/rb_call_credentials.c +297 -0
- data/src/ruby/ext/grpc/rb_call_credentials.h +31 -0
- data/src/ruby/ext/grpc/rb_channel.c +835 -0
- data/src/ruby/ext/grpc/rb_channel.h +34 -0
- data/src/ruby/ext/grpc/rb_channel_args.c +155 -0
- data/src/ruby/ext/grpc/rb_channel_args.h +38 -0
- data/src/ruby/ext/grpc/rb_channel_credentials.c +267 -0
- data/src/ruby/ext/grpc/rb_channel_credentials.h +32 -0
- data/src/ruby/ext/grpc/rb_completion_queue.c +100 -0
- data/src/ruby/ext/grpc/rb_completion_queue.h +36 -0
- data/src/ruby/ext/grpc/rb_compression_options.c +470 -0
- data/src/ruby/ext/grpc/rb_compression_options.h +29 -0
- data/src/ruby/ext/grpc/rb_enable_cpp.cc +22 -0
- data/src/ruby/ext/grpc/rb_event_thread.c +143 -0
- data/src/ruby/ext/grpc/rb_event_thread.h +21 -0
- data/src/ruby/ext/grpc/rb_grpc.c +328 -0
- data/src/ruby/ext/grpc/rb_grpc.h +76 -0
- data/src/ruby/ext/grpc/rb_grpc_imports.generated.c +573 -0
- data/src/ruby/ext/grpc/rb_grpc_imports.generated.h +865 -0
- data/src/ruby/ext/grpc/rb_loader.c +57 -0
- data/src/ruby/ext/grpc/rb_loader.h +25 -0
- data/src/ruby/ext/grpc/rb_server.c +372 -0
- data/src/ruby/ext/grpc/rb_server.h +32 -0
- data/src/ruby/ext/grpc/rb_server_credentials.c +243 -0
- data/src/ruby/ext/grpc/rb_server_credentials.h +32 -0
- data/src/ruby/lib/grpc.rb +37 -0
- data/src/ruby/lib/grpc/2.3/grpc_c.so +0 -0
- data/src/ruby/lib/grpc/2.4/grpc_c.so +0 -0
- data/src/ruby/lib/grpc/2.5/grpc_c.so +0 -0
- data/src/ruby/lib/grpc/2.6/grpc_c.so +0 -0
- data/src/ruby/lib/grpc/2.7/grpc_c.so +0 -0
- data/src/ruby/lib/grpc/core/status_codes.rb +135 -0
- data/src/ruby/lib/grpc/core/time_consts.rb +56 -0
- data/src/ruby/lib/grpc/errors.rb +277 -0
- data/src/ruby/lib/grpc/generic/active_call.rb +669 -0
- data/src/ruby/lib/grpc/generic/bidi_call.rb +233 -0
- data/src/ruby/lib/grpc/generic/client_stub.rb +501 -0
- data/src/ruby/lib/grpc/generic/interceptor_registry.rb +53 -0
- data/src/ruby/lib/grpc/generic/interceptors.rb +186 -0
- data/src/ruby/lib/grpc/generic/rpc_desc.rb +204 -0
- data/src/ruby/lib/grpc/generic/rpc_server.rb +551 -0
- data/src/ruby/lib/grpc/generic/service.rb +211 -0
- data/src/ruby/lib/grpc/google_rpc_status_utils.rb +40 -0
- data/src/ruby/lib/grpc/grpc.rb +24 -0
- data/src/ruby/lib/grpc/logconfig.rb +44 -0
- data/src/ruby/lib/grpc/notifier.rb +45 -0
- data/src/ruby/lib/grpc/structs.rb +15 -0
- data/src/ruby/lib/grpc/version.rb +18 -0
- data/src/ruby/pb/README.md +42 -0
- data/src/ruby/pb/generate_proto_ruby.sh +51 -0
- data/src/ruby/pb/grpc/health/checker.rb +75 -0
- data/src/ruby/pb/grpc/health/v1/health_pb.rb +31 -0
- data/src/ruby/pb/grpc/health/v1/health_services_pb.rb +62 -0
- data/src/ruby/pb/grpc/testing/duplicate/echo_duplicate_services_pb.rb +44 -0
- data/src/ruby/pb/grpc/testing/metrics_pb.rb +28 -0
- data/src/ruby/pb/grpc/testing/metrics_services_pb.rb +49 -0
- data/src/ruby/pb/src/proto/grpc/testing/empty_pb.rb +17 -0
- data/src/ruby/pb/src/proto/grpc/testing/messages_pb.rb +105 -0
- data/src/ruby/pb/src/proto/grpc/testing/test_pb.rb +16 -0
- data/src/ruby/pb/src/proto/grpc/testing/test_services_pb.rb +118 -0
- data/src/ruby/pb/test/client.rb +769 -0
- data/src/ruby/pb/test/server.rb +252 -0
- data/src/ruby/pb/test/xds_client.rb +213 -0
- data/src/ruby/spec/call_credentials_spec.rb +42 -0
- data/src/ruby/spec/call_spec.rb +180 -0
- data/src/ruby/spec/channel_connection_spec.rb +126 -0
- data/src/ruby/spec/channel_credentials_spec.rb +82 -0
- data/src/ruby/spec/channel_spec.rb +234 -0
- data/src/ruby/spec/client_auth_spec.rb +126 -0
- data/src/ruby/spec/client_server_spec.rb +664 -0
- data/src/ruby/spec/compression_options_spec.rb +149 -0
- data/src/ruby/spec/debug_message_spec.rb +134 -0
- data/src/ruby/spec/error_sanity_spec.rb +49 -0
- data/src/ruby/spec/errors_spec.rb +142 -0
- data/src/ruby/spec/generic/active_call_spec.rb +672 -0
- data/src/ruby/spec/generic/client_interceptors_spec.rb +153 -0
- data/src/ruby/spec/generic/client_stub_spec.rb +1083 -0
- data/src/ruby/spec/generic/interceptor_registry_spec.rb +65 -0
- data/src/ruby/spec/generic/rpc_desc_spec.rb +374 -0
- data/src/ruby/spec/generic/rpc_server_pool_spec.rb +127 -0
- data/src/ruby/spec/generic/rpc_server_spec.rb +748 -0
- data/src/ruby/spec/generic/server_interceptors_spec.rb +218 -0
- data/src/ruby/spec/generic/service_spec.rb +263 -0
- data/src/ruby/spec/google_rpc_status_utils_spec.rb +282 -0
- data/src/ruby/spec/pb/codegen/grpc/testing/package_options.proto +28 -0
- data/src/ruby/spec/pb/codegen/grpc/testing/package_options_import.proto +22 -0
- data/src/ruby/spec/pb/codegen/grpc/testing/package_options_import2.proto +23 -0
- data/src/ruby/spec/pb/codegen/grpc/testing/package_options_ruby_style.proto +41 -0
- data/src/ruby/spec/pb/codegen/package_option_spec.rb +82 -0
- data/src/ruby/spec/pb/duplicate/codegen_spec.rb +57 -0
- data/src/ruby/spec/pb/health/checker_spec.rb +236 -0
- data/src/ruby/spec/server_credentials_spec.rb +79 -0
- data/src/ruby/spec/server_spec.rb +209 -0
- data/src/ruby/spec/spec_helper.rb +61 -0
- data/src/ruby/spec/support/helpers.rb +107 -0
- data/src/ruby/spec/support/services.rb +160 -0
- data/src/ruby/spec/testdata/README +1 -0
- data/src/ruby/spec/testdata/ca.pem +20 -0
- data/src/ruby/spec/testdata/client.key +28 -0
- data/src/ruby/spec/testdata/client.pem +20 -0
- data/src/ruby/spec/testdata/server1.key +28 -0
- data/src/ruby/spec/testdata/server1.pem +22 -0
- data/src/ruby/spec/time_consts_spec.rb +74 -0
- metadata +394 -0
@@ -0,0 +1,126 @@
|
|
1
|
+
# Copyright 2015 gRPC authors.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require 'spec_helper'
|
16
|
+
|
17
|
+
def create_channel_creds
|
18
|
+
test_root = File.join(File.dirname(__FILE__), 'testdata')
|
19
|
+
files = ['ca.pem', 'client.key', 'client.pem']
|
20
|
+
creds = files.map { |f| File.open(File.join(test_root, f)).read }
|
21
|
+
GRPC::Core::ChannelCredentials.new(creds[0], creds[1], creds[2])
|
22
|
+
end
|
23
|
+
|
24
|
+
def client_cert
|
25
|
+
test_root = File.join(File.dirname(__FILE__), 'testdata')
|
26
|
+
cert = File.open(File.join(test_root, 'client.pem')).read
|
27
|
+
fail unless cert.is_a?(String)
|
28
|
+
cert
|
29
|
+
end
|
30
|
+
|
31
|
+
def create_server_creds
|
32
|
+
test_root = File.join(File.dirname(__FILE__), 'testdata')
|
33
|
+
GRPC.logger.info("test root: #{test_root}")
|
34
|
+
files = ['ca.pem', 'server1.key', 'server1.pem']
|
35
|
+
creds = files.map { |f| File.open(File.join(test_root, f)).read }
|
36
|
+
GRPC::Core::ServerCredentials.new(
|
37
|
+
creds[0],
|
38
|
+
[{ private_key: creds[1], cert_chain: creds[2] }],
|
39
|
+
true) # force client auth
|
40
|
+
end
|
41
|
+
|
42
|
+
# a test service that checks the cert of its peer
|
43
|
+
class SslTestService
|
44
|
+
include GRPC::GenericService
|
45
|
+
rpc :an_rpc, EchoMsg, EchoMsg
|
46
|
+
rpc :a_client_streaming_rpc, stream(EchoMsg), EchoMsg
|
47
|
+
rpc :a_server_streaming_rpc, EchoMsg, stream(EchoMsg)
|
48
|
+
rpc :a_bidi_rpc, stream(EchoMsg), stream(EchoMsg)
|
49
|
+
|
50
|
+
def check_peer_cert(call)
|
51
|
+
error_msg = "want:\n#{client_cert}\n\ngot:\n#{call.peer_cert}"
|
52
|
+
fail(error_msg) unless call.peer_cert == client_cert
|
53
|
+
end
|
54
|
+
|
55
|
+
def an_rpc(req, call)
|
56
|
+
check_peer_cert(call)
|
57
|
+
req
|
58
|
+
end
|
59
|
+
|
60
|
+
def a_client_streaming_rpc(call)
|
61
|
+
check_peer_cert(call)
|
62
|
+
call.each_remote_read.each { |r| GRPC.logger.info(r) }
|
63
|
+
EchoMsg.new
|
64
|
+
end
|
65
|
+
|
66
|
+
def a_server_streaming_rpc(_, call)
|
67
|
+
check_peer_cert(call)
|
68
|
+
[EchoMsg.new, EchoMsg.new]
|
69
|
+
end
|
70
|
+
|
71
|
+
def a_bidi_rpc(requests, call)
|
72
|
+
check_peer_cert(call)
|
73
|
+
requests.each { |r| GRPC.logger.info(r) }
|
74
|
+
[EchoMsg.new, EchoMsg.new]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
SslTestServiceStub = SslTestService.rpc_stub_class
|
79
|
+
|
80
|
+
describe 'client-server auth' do
|
81
|
+
RpcServer = GRPC::RpcServer
|
82
|
+
|
83
|
+
before(:all) do
|
84
|
+
server_opts = {
|
85
|
+
poll_period: 1
|
86
|
+
}
|
87
|
+
@srv = new_rpc_server_for_testing(**server_opts)
|
88
|
+
port = @srv.add_http2_port('0.0.0.0:0', create_server_creds)
|
89
|
+
@srv.handle(SslTestService)
|
90
|
+
@srv_thd = Thread.new { @srv.run }
|
91
|
+
@srv.wait_till_running
|
92
|
+
|
93
|
+
client_opts = {
|
94
|
+
channel_args: {
|
95
|
+
GRPC::Core::Channel::SSL_TARGET => 'foo.test.google.fr'
|
96
|
+
}
|
97
|
+
}
|
98
|
+
@stub = SslTestServiceStub.new("localhost:#{port}",
|
99
|
+
create_channel_creds,
|
100
|
+
**client_opts)
|
101
|
+
end
|
102
|
+
|
103
|
+
after(:all) do
|
104
|
+
expect(@srv.stopped?).to be(false)
|
105
|
+
@srv.stop
|
106
|
+
@srv_thd.join
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'client-server auth with unary RPCs' do
|
110
|
+
@stub.an_rpc(EchoMsg.new)
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'client-server auth with client streaming RPCs' do
|
114
|
+
@stub.a_client_streaming_rpc([EchoMsg.new, EchoMsg.new])
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'client-server auth with server streaming RPCs' do
|
118
|
+
responses = @stub.a_server_streaming_rpc(EchoMsg.new)
|
119
|
+
responses.each { |r| GRPC.logger.info(r) }
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'client-server auth with bidi RPCs' do
|
123
|
+
responses = @stub.a_bidi_rpc([EchoMsg.new, EchoMsg.new])
|
124
|
+
responses.each { |r| GRPC.logger.info(r) }
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,664 @@
|
|
1
|
+
# Copyright 2015 gRPC authors.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require 'spec_helper'
|
16
|
+
|
17
|
+
include GRPC::Core
|
18
|
+
|
19
|
+
shared_context 'setup: tags' do
|
20
|
+
let(:sent_message) { 'sent message' }
|
21
|
+
let(:reply_text) { 'the reply' }
|
22
|
+
|
23
|
+
def deadline
|
24
|
+
Time.now + 5
|
25
|
+
end
|
26
|
+
|
27
|
+
def server_allows_client_to_proceed(metadata = {})
|
28
|
+
recvd_rpc = @server.request_call
|
29
|
+
expect(recvd_rpc).to_not eq nil
|
30
|
+
server_call = recvd_rpc.call
|
31
|
+
ops = { CallOps::SEND_INITIAL_METADATA => metadata }
|
32
|
+
server_batch = server_call.run_batch(ops)
|
33
|
+
expect(server_batch.send_metadata).to be true
|
34
|
+
server_call
|
35
|
+
end
|
36
|
+
|
37
|
+
def new_client_call
|
38
|
+
@ch.create_call(nil, nil, '/method', nil, deadline)
|
39
|
+
end
|
40
|
+
|
41
|
+
def ok_status
|
42
|
+
Struct::Status.new(StatusCodes::OK, 'OK')
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
shared_examples 'basic GRPC message delivery is OK' do
|
47
|
+
include GRPC::Core
|
48
|
+
include_context 'setup: tags'
|
49
|
+
|
50
|
+
context 'the test channel' do
|
51
|
+
it 'should have a target' do
|
52
|
+
expect(@ch.target).to be_a(String)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'a client call' do
|
57
|
+
it 'should have a peer' do
|
58
|
+
expect(new_client_call.peer).to be_a(String)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'calls have peer info' do
|
63
|
+
call = new_client_call
|
64
|
+
expect(call.peer).to be_a(String)
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'servers receive requests from clients and can respond' do
|
68
|
+
call = new_client_call
|
69
|
+
server_call = nil
|
70
|
+
|
71
|
+
server_thread = Thread.new do
|
72
|
+
server_call = server_allows_client_to_proceed
|
73
|
+
end
|
74
|
+
|
75
|
+
client_ops = {
|
76
|
+
CallOps::SEND_INITIAL_METADATA => {},
|
77
|
+
CallOps::SEND_MESSAGE => sent_message,
|
78
|
+
CallOps::SEND_CLOSE_FROM_CLIENT => nil
|
79
|
+
}
|
80
|
+
client_batch = call.run_batch(client_ops)
|
81
|
+
expect(client_batch.send_metadata).to be true
|
82
|
+
expect(client_batch.send_message).to be true
|
83
|
+
expect(client_batch.send_close).to be true
|
84
|
+
|
85
|
+
# confirm the server can read the inbound message
|
86
|
+
server_thread.join
|
87
|
+
server_ops = {
|
88
|
+
CallOps::RECV_MESSAGE => nil,
|
89
|
+
CallOps::RECV_CLOSE_ON_SERVER => nil,
|
90
|
+
CallOps::SEND_STATUS_FROM_SERVER => ok_status
|
91
|
+
}
|
92
|
+
server_batch = server_call.run_batch(server_ops)
|
93
|
+
expect(server_batch.message).to eq(sent_message)
|
94
|
+
expect(server_batch.send_close).to be true
|
95
|
+
expect(server_batch.send_status).to be true
|
96
|
+
|
97
|
+
# finish the call
|
98
|
+
final_client_batch = call.run_batch(
|
99
|
+
CallOps::RECV_INITIAL_METADATA => nil,
|
100
|
+
CallOps::RECV_STATUS_ON_CLIENT => nil)
|
101
|
+
expect(final_client_batch.metadata).to eq({})
|
102
|
+
expect(final_client_batch.status.code).to eq(0)
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'responses written by servers are received by the client' do
|
106
|
+
call = new_client_call
|
107
|
+
server_call = nil
|
108
|
+
|
109
|
+
server_thread = Thread.new do
|
110
|
+
server_call = server_allows_client_to_proceed
|
111
|
+
end
|
112
|
+
|
113
|
+
client_ops = {
|
114
|
+
CallOps::SEND_INITIAL_METADATA => {},
|
115
|
+
CallOps::SEND_MESSAGE => sent_message,
|
116
|
+
CallOps::SEND_CLOSE_FROM_CLIENT => nil
|
117
|
+
}
|
118
|
+
client_batch = call.run_batch(client_ops)
|
119
|
+
expect(client_batch.send_metadata).to be true
|
120
|
+
expect(client_batch.send_message).to be true
|
121
|
+
expect(client_batch.send_close).to be true
|
122
|
+
|
123
|
+
# confirm the server can read the inbound message
|
124
|
+
server_thread.join
|
125
|
+
server_ops = {
|
126
|
+
CallOps::RECV_MESSAGE => nil,
|
127
|
+
CallOps::RECV_CLOSE_ON_SERVER => nil,
|
128
|
+
CallOps::SEND_MESSAGE => reply_text,
|
129
|
+
CallOps::SEND_STATUS_FROM_SERVER => ok_status
|
130
|
+
}
|
131
|
+
server_batch = server_call.run_batch(server_ops)
|
132
|
+
expect(server_batch.message).to eq(sent_message)
|
133
|
+
expect(server_batch.send_close).to be true
|
134
|
+
expect(server_batch.send_message).to be true
|
135
|
+
expect(server_batch.send_status).to be true
|
136
|
+
|
137
|
+
# finish the call
|
138
|
+
final_client_batch = call.run_batch(
|
139
|
+
CallOps::RECV_INITIAL_METADATA => nil,
|
140
|
+
CallOps::RECV_MESSAGE => nil,
|
141
|
+
CallOps::RECV_STATUS_ON_CLIENT => nil)
|
142
|
+
expect(final_client_batch.metadata).to eq({})
|
143
|
+
expect(final_client_batch.message).to eq(reply_text)
|
144
|
+
expect(final_client_batch.status.code).to eq(0)
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'compressed messages can be sent and received' do
|
148
|
+
call = new_client_call
|
149
|
+
server_call = nil
|
150
|
+
long_request_str = '0' * 2000
|
151
|
+
long_response_str = '1' * 2000
|
152
|
+
md = { 'grpc-internal-encoding-request' => 'gzip' }
|
153
|
+
|
154
|
+
server_thread = Thread.new do
|
155
|
+
server_call = server_allows_client_to_proceed(md)
|
156
|
+
end
|
157
|
+
|
158
|
+
client_ops = {
|
159
|
+
CallOps::SEND_INITIAL_METADATA => md,
|
160
|
+
CallOps::SEND_MESSAGE => long_request_str,
|
161
|
+
CallOps::SEND_CLOSE_FROM_CLIENT => nil
|
162
|
+
}
|
163
|
+
client_batch = call.run_batch(client_ops)
|
164
|
+
expect(client_batch.send_metadata).to be true
|
165
|
+
expect(client_batch.send_message).to be true
|
166
|
+
expect(client_batch.send_close).to be true
|
167
|
+
|
168
|
+
# confirm the server can read the inbound message
|
169
|
+
server_thread.join
|
170
|
+
server_ops = {
|
171
|
+
CallOps::RECV_MESSAGE => nil,
|
172
|
+
CallOps::RECV_CLOSE_ON_SERVER => nil,
|
173
|
+
CallOps::SEND_MESSAGE => long_response_str,
|
174
|
+
CallOps::SEND_STATUS_FROM_SERVER => ok_status
|
175
|
+
}
|
176
|
+
server_batch = server_call.run_batch(server_ops)
|
177
|
+
expect(server_batch.message).to eq(long_request_str)
|
178
|
+
expect(server_batch.send_close).to be true
|
179
|
+
expect(server_batch.send_message).to be true
|
180
|
+
expect(server_batch.send_status).to be true
|
181
|
+
|
182
|
+
client_ops = {
|
183
|
+
CallOps::RECV_INITIAL_METADATA => nil,
|
184
|
+
CallOps::RECV_MESSAGE => nil,
|
185
|
+
CallOps::RECV_STATUS_ON_CLIENT => nil
|
186
|
+
}
|
187
|
+
final_client_batch = call.run_batch(client_ops)
|
188
|
+
expect(final_client_batch.metadata).to eq({})
|
189
|
+
expect(final_client_batch.message).to eq long_response_str
|
190
|
+
expect(final_client_batch.status.code).to eq(0)
|
191
|
+
end
|
192
|
+
|
193
|
+
it 'servers can ignore a client write and send a status' do
|
194
|
+
call = new_client_call
|
195
|
+
server_call = nil
|
196
|
+
|
197
|
+
server_thread = Thread.new do
|
198
|
+
server_call = server_allows_client_to_proceed
|
199
|
+
end
|
200
|
+
|
201
|
+
client_ops = {
|
202
|
+
CallOps::SEND_INITIAL_METADATA => {},
|
203
|
+
CallOps::SEND_MESSAGE => sent_message,
|
204
|
+
CallOps::SEND_CLOSE_FROM_CLIENT => nil
|
205
|
+
}
|
206
|
+
client_batch = call.run_batch(client_ops)
|
207
|
+
expect(client_batch.send_metadata).to be true
|
208
|
+
expect(client_batch.send_message).to be true
|
209
|
+
expect(client_batch.send_close).to be true
|
210
|
+
|
211
|
+
# confirm the server can read the inbound message
|
212
|
+
the_status = Struct::Status.new(StatusCodes::OK, 'OK')
|
213
|
+
server_thread.join
|
214
|
+
server_ops = {
|
215
|
+
CallOps::SEND_STATUS_FROM_SERVER => the_status
|
216
|
+
}
|
217
|
+
server_batch = server_call.run_batch(server_ops)
|
218
|
+
expect(server_batch.message).to eq nil
|
219
|
+
expect(server_batch.send_status).to be true
|
220
|
+
|
221
|
+
final_client_batch = call.run_batch(
|
222
|
+
CallOps::RECV_INITIAL_METADATA => nil,
|
223
|
+
CallOps::RECV_STATUS_ON_CLIENT => nil)
|
224
|
+
expect(final_client_batch.metadata).to eq({})
|
225
|
+
expect(final_client_batch.status.code).to eq(0)
|
226
|
+
end
|
227
|
+
|
228
|
+
it 'completes calls by sending status to client and server' do
|
229
|
+
call = new_client_call
|
230
|
+
server_call = nil
|
231
|
+
|
232
|
+
server_thread = Thread.new do
|
233
|
+
server_call = server_allows_client_to_proceed
|
234
|
+
end
|
235
|
+
|
236
|
+
client_ops = {
|
237
|
+
CallOps::SEND_INITIAL_METADATA => {},
|
238
|
+
CallOps::SEND_MESSAGE => sent_message
|
239
|
+
}
|
240
|
+
client_batch = call.run_batch(client_ops)
|
241
|
+
expect(client_batch.send_metadata).to be true
|
242
|
+
expect(client_batch.send_message).to be true
|
243
|
+
|
244
|
+
# confirm the server can read the inbound message and respond
|
245
|
+
the_status = Struct::Status.new(StatusCodes::OK, 'OK', {})
|
246
|
+
server_thread.join
|
247
|
+
server_ops = {
|
248
|
+
CallOps::RECV_MESSAGE => nil,
|
249
|
+
CallOps::SEND_MESSAGE => reply_text,
|
250
|
+
CallOps::SEND_STATUS_FROM_SERVER => the_status
|
251
|
+
}
|
252
|
+
server_batch = server_call.run_batch(server_ops)
|
253
|
+
expect(server_batch.message).to eq sent_message
|
254
|
+
expect(server_batch.send_status).to be true
|
255
|
+
expect(server_batch.send_message).to be true
|
256
|
+
|
257
|
+
# confirm the client can receive the server response and status.
|
258
|
+
client_ops = {
|
259
|
+
CallOps::SEND_CLOSE_FROM_CLIENT => nil,
|
260
|
+
CallOps::RECV_INITIAL_METADATA => nil,
|
261
|
+
CallOps::RECV_MESSAGE => nil,
|
262
|
+
CallOps::RECV_STATUS_ON_CLIENT => nil
|
263
|
+
}
|
264
|
+
final_client_batch = call.run_batch(client_ops)
|
265
|
+
expect(final_client_batch.send_close).to be true
|
266
|
+
expect(final_client_batch.message).to eq reply_text
|
267
|
+
expect(final_client_batch.status).to eq the_status
|
268
|
+
|
269
|
+
# confirm the server can receive the client close.
|
270
|
+
server_ops = {
|
271
|
+
CallOps::RECV_CLOSE_ON_SERVER => nil
|
272
|
+
}
|
273
|
+
final_server_batch = server_call.run_batch(server_ops)
|
274
|
+
expect(final_server_batch.send_close).to be true
|
275
|
+
end
|
276
|
+
|
277
|
+
def client_cancel_test(cancel_proc, expected_code,
|
278
|
+
expected_details)
|
279
|
+
call = new_client_call
|
280
|
+
server_call = nil
|
281
|
+
|
282
|
+
server_thread = Thread.new do
|
283
|
+
server_call = server_allows_client_to_proceed
|
284
|
+
end
|
285
|
+
|
286
|
+
client_ops = {
|
287
|
+
CallOps::SEND_INITIAL_METADATA => {},
|
288
|
+
CallOps::RECV_INITIAL_METADATA => nil
|
289
|
+
}
|
290
|
+
client_batch = call.run_batch(client_ops)
|
291
|
+
expect(client_batch.send_metadata).to be true
|
292
|
+
expect(client_batch.metadata).to eq({})
|
293
|
+
|
294
|
+
cancel_proc.call(call)
|
295
|
+
|
296
|
+
server_thread.join
|
297
|
+
server_ops = {
|
298
|
+
CallOps::RECV_CLOSE_ON_SERVER => nil
|
299
|
+
}
|
300
|
+
server_batch = server_call.run_batch(server_ops)
|
301
|
+
expect(server_batch.send_close).to be true
|
302
|
+
|
303
|
+
client_ops = {
|
304
|
+
CallOps::RECV_STATUS_ON_CLIENT => {}
|
305
|
+
}
|
306
|
+
client_batch = call.run_batch(client_ops)
|
307
|
+
|
308
|
+
expect(client_batch.status.code).to be expected_code
|
309
|
+
expect(client_batch.status.details).to eq expected_details
|
310
|
+
end
|
311
|
+
|
312
|
+
it 'clients can cancel a call on the server' do
|
313
|
+
expected_code = StatusCodes::CANCELLED
|
314
|
+
expected_details = 'Cancelled'
|
315
|
+
cancel_proc = proc { |call| call.cancel }
|
316
|
+
client_cancel_test(cancel_proc, expected_code, expected_details)
|
317
|
+
end
|
318
|
+
|
319
|
+
it 'cancel_with_status unknown status' do
|
320
|
+
code = StatusCodes::UNKNOWN
|
321
|
+
details = 'test unknown reason'
|
322
|
+
cancel_proc = proc { |call| call.cancel_with_status(code, details) }
|
323
|
+
client_cancel_test(cancel_proc, code, details)
|
324
|
+
end
|
325
|
+
|
326
|
+
it 'cancel_with_status unknown status' do
|
327
|
+
code = StatusCodes::FAILED_PRECONDITION
|
328
|
+
details = 'test failed precondition reason'
|
329
|
+
cancel_proc = proc { |call| call.cancel_with_status(code, details) }
|
330
|
+
client_cancel_test(cancel_proc, code, details)
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
shared_examples 'GRPC metadata delivery works OK' do
|
335
|
+
include_context 'setup: tags'
|
336
|
+
|
337
|
+
describe 'from client => server' do
|
338
|
+
before(:example) do
|
339
|
+
n = 7 # arbitrary number of metadata
|
340
|
+
diff_keys_fn = proc { |i| [format('k%d', i), format('v%d', i)] }
|
341
|
+
diff_keys = Hash[n.times.collect { |x| diff_keys_fn.call x }]
|
342
|
+
null_vals_fn = proc { |i| [format('k%d', i), format('v\0%d', i)] }
|
343
|
+
null_vals = Hash[n.times.collect { |x| null_vals_fn.call x }]
|
344
|
+
same_keys_fn = proc { |i| [format('k%d', i), [format('v%d', i)] * n] }
|
345
|
+
same_keys = Hash[n.times.collect { |x| same_keys_fn.call x }]
|
346
|
+
symbol_key = { a_key: 'a val' }
|
347
|
+
@valid_metadata = [diff_keys, same_keys, null_vals, symbol_key]
|
348
|
+
@bad_keys = []
|
349
|
+
@bad_keys << { Object.new => 'a value' }
|
350
|
+
@bad_keys << { 1 => 'a value' }
|
351
|
+
end
|
352
|
+
|
353
|
+
it 'raises an exception if a metadata key is invalid' do
|
354
|
+
@bad_keys.each do |md|
|
355
|
+
call = new_client_call
|
356
|
+
client_ops = {
|
357
|
+
CallOps::SEND_INITIAL_METADATA => md
|
358
|
+
}
|
359
|
+
blk = proc do
|
360
|
+
call.run_batch(client_ops)
|
361
|
+
end
|
362
|
+
expect(&blk).to raise_error
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
it 'sends all the metadata pairs when keys and values are valid' do
|
367
|
+
@valid_metadata.each do |md|
|
368
|
+
recvd_rpc = nil
|
369
|
+
rcv_thread = Thread.new do
|
370
|
+
recvd_rpc = @server.request_call
|
371
|
+
end
|
372
|
+
|
373
|
+
call = new_client_call
|
374
|
+
client_ops = {
|
375
|
+
CallOps::SEND_INITIAL_METADATA => md,
|
376
|
+
CallOps::SEND_CLOSE_FROM_CLIENT => nil
|
377
|
+
}
|
378
|
+
client_batch = call.run_batch(client_ops)
|
379
|
+
expect(client_batch.send_metadata).to be true
|
380
|
+
|
381
|
+
# confirm the server can receive the client metadata
|
382
|
+
rcv_thread.join
|
383
|
+
expect(recvd_rpc).to_not eq nil
|
384
|
+
recvd_md = recvd_rpc.metadata
|
385
|
+
replace_symbols = Hash[md.each_pair.collect { |x, y| [x.to_s, y] }]
|
386
|
+
expect(recvd_md).to eq(recvd_md.merge(replace_symbols))
|
387
|
+
|
388
|
+
# finish the call
|
389
|
+
final_server_batch = recvd_rpc.call.run_batch(
|
390
|
+
CallOps::RECV_CLOSE_ON_SERVER => nil,
|
391
|
+
CallOps::SEND_INITIAL_METADATA => nil,
|
392
|
+
CallOps::SEND_STATUS_FROM_SERVER => ok_status)
|
393
|
+
expect(final_server_batch.send_close).to be(true)
|
394
|
+
expect(final_server_batch.send_metadata).to be(true)
|
395
|
+
expect(final_server_batch.send_status).to be(true)
|
396
|
+
|
397
|
+
final_client_batch = call.run_batch(
|
398
|
+
CallOps::RECV_INITIAL_METADATA => nil,
|
399
|
+
CallOps::RECV_STATUS_ON_CLIENT => nil)
|
400
|
+
expect(final_client_batch.metadata).to eq({})
|
401
|
+
expect(final_client_batch.status.code).to eq(0)
|
402
|
+
end
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
describe 'from server => client' do
|
407
|
+
before(:example) do
|
408
|
+
n = 7 # arbitrary number of metadata
|
409
|
+
diff_keys_fn = proc { |i| [format('k%d', i), format('v%d', i)] }
|
410
|
+
diff_keys = Hash[n.times.collect { |x| diff_keys_fn.call x }]
|
411
|
+
null_vals_fn = proc { |i| [format('k%d', i), format('v\0%d', i)] }
|
412
|
+
null_vals = Hash[n.times.collect { |x| null_vals_fn.call x }]
|
413
|
+
same_keys_fn = proc { |i| [format('k%d', i), [format('v%d', i)] * n] }
|
414
|
+
same_keys = Hash[n.times.collect { |x| same_keys_fn.call x }]
|
415
|
+
symbol_key = { a_key: 'a val' }
|
416
|
+
@valid_metadata = [diff_keys, same_keys, null_vals, symbol_key]
|
417
|
+
@bad_keys = []
|
418
|
+
@bad_keys << { Object.new => 'a value' }
|
419
|
+
@bad_keys << { 1 => 'a value' }
|
420
|
+
end
|
421
|
+
|
422
|
+
it 'raises an exception if a metadata key is invalid' do
|
423
|
+
@bad_keys.each do |md|
|
424
|
+
recvd_rpc = nil
|
425
|
+
rcv_thread = Thread.new do
|
426
|
+
recvd_rpc = @server.request_call
|
427
|
+
end
|
428
|
+
|
429
|
+
call = new_client_call
|
430
|
+
# client signals that it's done sending metadata to allow server to
|
431
|
+
# respond
|
432
|
+
client_ops = {
|
433
|
+
CallOps::SEND_INITIAL_METADATA => nil
|
434
|
+
}
|
435
|
+
call.run_batch(client_ops)
|
436
|
+
|
437
|
+
# server gets the invocation
|
438
|
+
rcv_thread.join
|
439
|
+
expect(recvd_rpc).to_not eq nil
|
440
|
+
server_ops = {
|
441
|
+
CallOps::SEND_INITIAL_METADATA => md
|
442
|
+
}
|
443
|
+
blk = proc do
|
444
|
+
recvd_rpc.call.run_batch(server_ops)
|
445
|
+
end
|
446
|
+
expect(&blk).to raise_error
|
447
|
+
|
448
|
+
# cancel the call so the server can shut down immediately
|
449
|
+
call.cancel
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
it 'sends an empty hash if no metadata is added' do
|
454
|
+
recvd_rpc = nil
|
455
|
+
rcv_thread = Thread.new do
|
456
|
+
recvd_rpc = @server.request_call
|
457
|
+
end
|
458
|
+
|
459
|
+
call = new_client_call
|
460
|
+
# client signals that it's done sending metadata to allow server to
|
461
|
+
# respond
|
462
|
+
client_ops = {
|
463
|
+
CallOps::SEND_INITIAL_METADATA => nil,
|
464
|
+
CallOps::SEND_CLOSE_FROM_CLIENT => nil
|
465
|
+
}
|
466
|
+
client_batch = call.run_batch(client_ops)
|
467
|
+
expect(client_batch.send_metadata).to be true
|
468
|
+
expect(client_batch.send_close).to be true
|
469
|
+
|
470
|
+
# server gets the invocation but sends no metadata back
|
471
|
+
rcv_thread.join
|
472
|
+
expect(recvd_rpc).to_not eq nil
|
473
|
+
server_call = recvd_rpc.call
|
474
|
+
server_ops = {
|
475
|
+
# receive close and send status to finish the call
|
476
|
+
CallOps::RECV_CLOSE_ON_SERVER => nil,
|
477
|
+
CallOps::SEND_INITIAL_METADATA => nil,
|
478
|
+
CallOps::SEND_STATUS_FROM_SERVER => ok_status
|
479
|
+
}
|
480
|
+
srv_batch = server_call.run_batch(server_ops)
|
481
|
+
expect(srv_batch.send_close).to be true
|
482
|
+
expect(srv_batch.send_metadata).to be true
|
483
|
+
expect(srv_batch.send_status).to be true
|
484
|
+
|
485
|
+
# client receives nothing as expected
|
486
|
+
client_ops = {
|
487
|
+
CallOps::RECV_INITIAL_METADATA => nil,
|
488
|
+
# receive status to finish the call
|
489
|
+
CallOps::RECV_STATUS_ON_CLIENT => nil
|
490
|
+
}
|
491
|
+
final_client_batch = call.run_batch(client_ops)
|
492
|
+
expect(final_client_batch.metadata).to eq({})
|
493
|
+
expect(final_client_batch.status.code).to eq(0)
|
494
|
+
end
|
495
|
+
|
496
|
+
it 'sends all the pairs when keys and values are valid' do
|
497
|
+
@valid_metadata.each do |md|
|
498
|
+
recvd_rpc = nil
|
499
|
+
rcv_thread = Thread.new do
|
500
|
+
recvd_rpc = @server.request_call
|
501
|
+
end
|
502
|
+
|
503
|
+
call = new_client_call
|
504
|
+
# client signals that it's done sending metadata to allow server to
|
505
|
+
# respond
|
506
|
+
client_ops = {
|
507
|
+
CallOps::SEND_INITIAL_METADATA => nil,
|
508
|
+
CallOps::SEND_CLOSE_FROM_CLIENT => nil
|
509
|
+
}
|
510
|
+
client_batch = call.run_batch(client_ops)
|
511
|
+
expect(client_batch.send_metadata).to be true
|
512
|
+
expect(client_batch.send_close).to be true
|
513
|
+
|
514
|
+
# server gets the invocation but sends no metadata back
|
515
|
+
rcv_thread.join
|
516
|
+
expect(recvd_rpc).to_not eq nil
|
517
|
+
server_call = recvd_rpc.call
|
518
|
+
server_ops = {
|
519
|
+
CallOps::RECV_CLOSE_ON_SERVER => nil,
|
520
|
+
CallOps::SEND_INITIAL_METADATA => md,
|
521
|
+
CallOps::SEND_STATUS_FROM_SERVER => ok_status
|
522
|
+
}
|
523
|
+
srv_batch = server_call.run_batch(server_ops)
|
524
|
+
expect(srv_batch.send_close).to be true
|
525
|
+
expect(srv_batch.send_metadata).to be true
|
526
|
+
expect(srv_batch.send_status).to be true
|
527
|
+
|
528
|
+
# client receives nothing as expected
|
529
|
+
client_ops = {
|
530
|
+
CallOps::RECV_INITIAL_METADATA => nil,
|
531
|
+
CallOps::RECV_STATUS_ON_CLIENT => nil
|
532
|
+
}
|
533
|
+
final_client_batch = call.run_batch(client_ops)
|
534
|
+
replace_symbols = Hash[md.each_pair.collect { |x, y| [x.to_s, y] }]
|
535
|
+
expect(final_client_batch.metadata).to eq(replace_symbols)
|
536
|
+
expect(final_client_batch.status.code).to eq(0)
|
537
|
+
end
|
538
|
+
end
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
542
|
+
describe 'the http client/server' do
|
543
|
+
before(:example) do
|
544
|
+
server_host = '0.0.0.0:0'
|
545
|
+
@server = new_core_server_for_testing(nil)
|
546
|
+
server_port = @server.add_http2_port(server_host, :this_port_is_insecure)
|
547
|
+
@server.start
|
548
|
+
@ch = Channel.new("0.0.0.0:#{server_port}", nil, :this_channel_is_insecure)
|
549
|
+
end
|
550
|
+
|
551
|
+
after(:example) do
|
552
|
+
@ch.close
|
553
|
+
@server.shutdown_and_notify(deadline)
|
554
|
+
@server.close
|
555
|
+
end
|
556
|
+
|
557
|
+
it_behaves_like 'basic GRPC message delivery is OK' do
|
558
|
+
end
|
559
|
+
|
560
|
+
it_behaves_like 'GRPC metadata delivery works OK' do
|
561
|
+
end
|
562
|
+
end
|
563
|
+
|
564
|
+
describe 'the secure http client/server' do
|
565
|
+
include_context 'setup: tags'
|
566
|
+
|
567
|
+
def load_test_certs
|
568
|
+
test_root = File.join(File.dirname(__FILE__), 'testdata')
|
569
|
+
files = ['ca.pem', 'server1.key', 'server1.pem']
|
570
|
+
files.map { |f| File.open(File.join(test_root, f)).read }
|
571
|
+
end
|
572
|
+
|
573
|
+
before(:example) do
|
574
|
+
certs = load_test_certs
|
575
|
+
server_host = '0.0.0.0:0'
|
576
|
+
server_creds = GRPC::Core::ServerCredentials.new(
|
577
|
+
nil, [{ private_key: certs[1], cert_chain: certs[2] }], false)
|
578
|
+
@server = new_core_server_for_testing(nil)
|
579
|
+
server_port = @server.add_http2_port(server_host, server_creds)
|
580
|
+
@server.start
|
581
|
+
args = { Channel::SSL_TARGET => 'foo.test.google.fr' }
|
582
|
+
@ch = Channel.new("0.0.0.0:#{server_port}", args,
|
583
|
+
GRPC::Core::ChannelCredentials.new(certs[0], nil, nil))
|
584
|
+
end
|
585
|
+
|
586
|
+
after(:example) do
|
587
|
+
@server.shutdown_and_notify(deadline)
|
588
|
+
@server.close
|
589
|
+
end
|
590
|
+
|
591
|
+
it_behaves_like 'basic GRPC message delivery is OK' do
|
592
|
+
end
|
593
|
+
|
594
|
+
it_behaves_like 'GRPC metadata delivery works OK' do
|
595
|
+
end
|
596
|
+
|
597
|
+
def credentials_update_test(creds_update_md)
|
598
|
+
auth_proc = proc { creds_update_md }
|
599
|
+
call_creds = GRPC::Core::CallCredentials.new(auth_proc)
|
600
|
+
|
601
|
+
initial_md_key = 'k2'
|
602
|
+
initial_md_val = 'v2'
|
603
|
+
initial_md = { initial_md_key => initial_md_val }
|
604
|
+
expected_md = creds_update_md.clone
|
605
|
+
fail 'bad test param' unless expected_md[initial_md_key].nil?
|
606
|
+
expected_md[initial_md_key] = initial_md_val
|
607
|
+
|
608
|
+
recvd_rpc = nil
|
609
|
+
rcv_thread = Thread.new do
|
610
|
+
recvd_rpc = @server.request_call
|
611
|
+
end
|
612
|
+
|
613
|
+
call = new_client_call
|
614
|
+
call.set_credentials! call_creds
|
615
|
+
|
616
|
+
client_batch = call.run_batch(
|
617
|
+
CallOps::SEND_INITIAL_METADATA => initial_md,
|
618
|
+
CallOps::SEND_CLOSE_FROM_CLIENT => nil)
|
619
|
+
expect(client_batch.send_metadata).to be true
|
620
|
+
expect(client_batch.send_close).to be true
|
621
|
+
|
622
|
+
# confirm the server can receive the client metadata
|
623
|
+
rcv_thread.join
|
624
|
+
expect(recvd_rpc).to_not eq nil
|
625
|
+
recvd_md = recvd_rpc.metadata
|
626
|
+
replace_symbols = Hash[expected_md.each_pair.collect { |x, y| [x.to_s, y] }]
|
627
|
+
expect(recvd_md).to eq(recvd_md.merge(replace_symbols))
|
628
|
+
|
629
|
+
credentials_update_test_finish_call(call, recvd_rpc.call)
|
630
|
+
end
|
631
|
+
|
632
|
+
def credentials_update_test_finish_call(client_call, server_call)
|
633
|
+
final_server_batch = server_call.run_batch(
|
634
|
+
CallOps::RECV_CLOSE_ON_SERVER => nil,
|
635
|
+
CallOps::SEND_INITIAL_METADATA => nil,
|
636
|
+
CallOps::SEND_STATUS_FROM_SERVER => ok_status)
|
637
|
+
expect(final_server_batch.send_close).to be(true)
|
638
|
+
expect(final_server_batch.send_metadata).to be(true)
|
639
|
+
expect(final_server_batch.send_status).to be(true)
|
640
|
+
|
641
|
+
final_client_batch = client_call.run_batch(
|
642
|
+
CallOps::RECV_INITIAL_METADATA => nil,
|
643
|
+
CallOps::RECV_STATUS_ON_CLIENT => nil)
|
644
|
+
expect(final_client_batch.metadata).to eq({})
|
645
|
+
expect(final_client_batch.status.code).to eq(0)
|
646
|
+
end
|
647
|
+
|
648
|
+
it 'modifies metadata with CallCredentials' do
|
649
|
+
credentials_update_test('k1' => 'updated-v1')
|
650
|
+
end
|
651
|
+
|
652
|
+
it 'modifies large metadata with CallCredentials' do
|
653
|
+
val_array = %w(
|
654
|
+
'00000000000000000000000000000000000000000000000000000000000000',
|
655
|
+
'11111111111111111111111111111111111111111111111111111111111111',
|
656
|
+
)
|
657
|
+
md = {
|
658
|
+
k3: val_array,
|
659
|
+
k4: '0000000000000000000000000000000000000000000000000000000000',
|
660
|
+
keeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeey5: 'v1'
|
661
|
+
}
|
662
|
+
credentials_update_test(md)
|
663
|
+
end
|
664
|
+
end
|