diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml new file mode 100644 index 0000000..d77d3a0 --- /dev/null +++ b/.github/workflows/TagBot.yml @@ -0,0 +1,11 @@ +name: TagBot +on: + schedule: + - cron: 0 * * * * +jobs: + TagBot: + runs-on: ubuntu-latest + steps: + - uses: JuliaRegistries/TagBot@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..519c4b0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,80 @@ +name: CI +on: + push: + branches: [master] + tags: ["*"] + pull_request: +env: + GO111MODULE: on +jobs: + test: + name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + version: + - '1.3' + - '1' # automatically expands to the latest stable 1.x release of Julia + - nightly + os: + - ubuntu-latest + arch: + - x64 + - x86 + # include: + # # test macOS and Windows with latest Julia only + # - os: macOS-latest + # arch: x64 + # version: 1 + # - os: windows-latest + # arch: x64 + # version: 1 + # - os: windows-latest + # arch: x86 + # version: 1 + steps: + - uses: actions/checkout@v2 + - name: setup go + uses: actions/setup-go@v2 + with: + go-version: '1.12.9' + - run: go version + - name: setup protoc + uses: arduino/setup-protoc@v1 + with: + version: '3.x' + - run: protoc --version + - name: generate test certificates + run: test/certgen/certgen.sh + shell: bash + - name: install protoc-gen-go + run: | + go get google.golang.org/protobuf/cmd/protoc-gen-go google.golang.org/grpc/cmd/protoc-gen-go-grpc + shell: bash + - name: start test server + run: test/runserver.sh + shell: bash + - uses: julia-actions/setup-julia@v1 + with: + version: ${{ matrix.version }} + arch: ${{ matrix.arch }} + - uses: actions/cache@v1 + env: + cache-name: cache-artifacts + with: + path: ~/.julia/artifacts + key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} + restore-keys: | + ${{ runner.os }}-test-${{ env.cache-name }}- + ${{ runner.os }}-test- + ${{ runner.os }}- + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v1 + with: + file: lcov.info + - name: shutdown test server + run: kill `cat test/grpc-go/examples/route_guide/server.pid` + shell: bash \ No newline at end of file diff --git a/README.md b/README.md index 46bbbc1..363f8e5 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,12 @@ A Julia gRPC Client. +GitHub Actions : [![Build Status](https://github.com/JuliaComputing/gRPCClient.jl/workflows/CI/badge.svg)](https://github.com/JuliaComputing/gRPCClient.jl/actions?query=workflow%3ACI+branch%3Amaster) + +[![Coverage Status](https://coveralls.io/repos/JuliaComputing/gRPCClient.jl/badge.svg?branch=master)](https://coveralls.io/r/JuliaComputing/gRPCClient.jl?branch=master) +[![codecov.io](http://codecov.io/github/JuliaComputing/gRPCClient.jl/coverage.svg?branch=master)](http://codecov.io/github/JuliaComputing/gRPCClient.jl?branch=master) + + ## Generating gRPC Service Client gRPC services are declared in `.proto` files. Use `gRPCClient.generate` to generate client code from specification files. @@ -68,7 +74,6 @@ gRPCController(; [ keepalive::Int64 = 60, ] [ request_timeout::Real = Inf, ] [ connect_timeout::Real = 0, ] - [ verify_peer::Bool = true, ] [ verbose::Bool = false, ] ) ``` @@ -80,7 +85,6 @@ gRPCController(; - `request_timeout`: request timeout (seconds) - `connect_timeout`: connect timeout (seconds) (default is 300 seconds, same as setting this to 0) -- `verify_peer`: whether to verify the peer (server) certificate (default true) - `verbose`: whether to print out verbose communication logs (default false) ### `gRPCChannel` diff --git a/src/curl.jl b/src/curl.jl index aef54fd..4261897 100644 --- a/src/curl.jl +++ b/src/curl.jl @@ -1,5 +1,23 @@ const GRPC_STATIC_HEADERS = Ref{Ptr{Nothing}}(C_NULL) +#= +const SEND_BUFFER_SZ = 1024 * 1024 +function buffer_send_data(input::Channel{T}) where T <: ProtoType + data = nothing + if isready(input) + iob = IOBuffer() + while isready(input) && (iob.size < SEND_BUFFER_SZ) + write(iob, to_delimited_message_bytes(take!(input))) + yield() + end + data = take!(iob) + elseif isopen(input) + data = UInt8[] + end + data +end +=# + function send_data(easy::Curl.Easy, input::Channel{T}) where T <: ProtoType while true data = isready(input) ? to_delimited_message_bytes(take!(input)) : isopen(input) ? UInt8[] : nothing @@ -20,20 +38,19 @@ function grpc_headers() headers end -function easy_handle(maxage, keepalive, verify_peer) +function easy_handle(maxage::Clong, keepalive::Clong) easy = Curl.Easy() Curl.setopt(easy, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0) Curl.setopt(easy, CURLOPT_PIPEWAIT, Clong(1)) Curl.setopt(easy, CURLOPT_POST, Clong(1)) Curl.setopt(easy, CURLOPT_HTTPHEADER, GRPC_STATIC_HEADERS[]) - Curl.set_ssl_verify(easy, verify_peer) if maxage > 0 - Curl.setopt(easy, CURLOPT_MAXAGE_CONN, Clong(maxage)) + Curl.setopt(easy, CURLOPT_MAXAGE_CONN, maxage) end if keepalive > 0 Curl.setopt(easy, CURLOPT_TCP_KEEPALIVE, Clong(1)) - Curl.setopt(easy, CURLOPT_TCP_KEEPINTVL, Clong(keepalive)); - Curl.setopt(easy, CURLOPT_TCP_KEEPIDLE, Clong(keepalive)); + Curl.setopt(easy, CURLOPT_TCP_KEEPINTVL, keepalive); + Curl.setopt(easy, CURLOPT_TCP_KEEPIDLE, keepalive); end easy end @@ -87,13 +104,12 @@ function set_connect_timeout(easy::Curl.Easy, timeout::Real) end function grpc_request(downloader::Downloader, url::String, input::Channel{T1}, output::Channel{T2}; - maxage::Int64 = typemax(Int64), - keepalive::Int64 = 60, + maxage::Clong = typemax(Clong), + keepalive::Clong = 60, request_timeout::Real = Inf, connect_timeout::Real = 0, - verify_peer::Bool = true, verbose::Bool = false)::gRPCStatus where {T1 <: ProtoType, T2 <: ProtoType} - Curl.with_handle(easy_handle(maxage, keepalive, verify_peer)) do easy + Curl.with_handle(easy_handle(maxage, keepalive)) do easy # setup the request Curl.set_url(easy, url) Curl.set_timeout(easy, request_timeout) diff --git a/src/grpc.jl b/src/grpc.jl index 8e87e03..11aa010 100644 --- a/src/grpc.jl +++ b/src/grpc.jl @@ -53,7 +53,6 @@ end [ keepalive::Int64 = 60, ] [ request_timeout::Real = Inf, ] [ connect_timeout::Real = 0, ] - [ verify_peer::Bool = true, ] [ verbose::Bool = false, ] ) @@ -65,7 +64,6 @@ Contains settings to control the behavior of gRPC requests. - `request_timeout`: request timeout (seconds) - `connect_timeout`: connect timeout (seconds) (default is 300 seconds, same as setting this to 0) -- `verify_peer`: whether to verify the peer (server) certificate (default true) - `verbose`: whether to print out verbose communication logs (default false) """ struct gRPCController <: ProtoRpcController @@ -73,18 +71,16 @@ struct gRPCController <: ProtoRpcController keepalive::Clong request_timeout::Real connect_timeout::Real - verify_peer::Bool verbose::Bool function gRPCController(; - maxage::Int = 0, - keepalive::Int64 = 60, + maxage::Integer = 0, + keepalive::Integer = 60, request_timeout::Real = Inf, connect_timeout::Real = 0, - verify_peer::Bool = true, verbose::Bool = false ) - new(maxage, keepalive, request_timeout, connect_timeout, verify_peer, verbose) + new(maxage, keepalive, request_timeout, connect_timeout, verbose) end end @@ -147,7 +143,6 @@ function call_method(channel::gRPCChannel, service::ServiceDescriptor, method::M keepalive = controller.keepalive, request_timeout = controller.request_timeout, connect_timeout = controller.connect_timeout, - verify_peer = controller.verify_peer, verbose = controller.verbose, ) outchannel, status_future diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 0000000..1a7d908 --- /dev/null +++ b/test/.gitignore @@ -0,0 +1,2 @@ +grpc-go +server.pid diff --git a/test/certgen/.gitignore b/test/certgen/.gitignore new file mode 100644 index 0000000..3d9e703 --- /dev/null +++ b/test/certgen/.gitignore @@ -0,0 +1,5 @@ +*.crt +*.key +*.pem +*.csr +*.srl diff --git a/test/certgen/certgen.sh b/test/certgen/certgen.sh new file mode 100755 index 0000000..8aa16fc --- /dev/null +++ b/test/certgen/certgen.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +BASEDIR=$(dirname $0) +cd $BASEDIR + +HOSTNAME=`hostname -f` +# HOSTNAME=localhost + +# Generate self signed root CA cert +openssl req -nodes -x509 -newkey rsa:2048 -keyout ca.key -out ca.crt -subj "/C=IN/ST=KA/L=Bangalore/O=JuliaComputing/OU=gRPCClient/CN=${HOSTNAME}/emailAddress=ca@examplegrpcclient.com" + +# Generate server cert to be signed +openssl req -nodes -newkey rsa:2048 -keyout server.key -out server.csr -subj "/C=IN/ST=KA/L=Bangalore/O=JuliaComputing/OU=server/CN=${HOSTNAME}/emailAddress=server@examplegrpcclient.com" + +# Sign the server cert +openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt + +# Create server PEM file +cat server.key server.crt > server.pem + + +# Generate client cert to be signed +openssl req -nodes -newkey rsa:2048 -keyout client.key -out client.csr -subj "/C=IN/ST=KA/L=Bangalore/O=JuliaComputing/OU=client/CN=${HOSTNAME}/emailAddress=client@examplegrpcclient.com" + +# Sign the client cert +openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAserial ca.srl -out client.crt + +# Create client PEM file +cat client.key client.crt > client.pem diff --git a/test/runserver.sh b/test/runserver.sh new file mode 100755 index 0000000..87e9691 --- /dev/null +++ b/test/runserver.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +BASEDIR=$(dirname $0) +cd ${BASEDIR} + +export PATH="$PATH:$(go env GOPATH)/bin" +CERT_FILE=../../../certgen/server.pem +KEY_FILE=../../../certgen/server.key +HOSTNAME=`hostname -f` + +git clone -b v1.35.0 https://github.com/grpc/grpc-go +cd grpc-go/examples/route_guide +protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative routeguide/route_guide.proto +sed 's/localhost/0.0.0.0/g' server/server.go > server/server.go.new +rm server/server.go +mv server/server.go.new server/server.go +go build -o runserver -i server/server.go + +./runserver --tls=true --cert_file=$CERT_FILE --key_file=$KEY_FILE & +echo $! > server.pid +echo "server pid `cat server.pid`" + +NEXT_WAIT_TIME=0 +until [ $NEXT_WAIT_TIME -eq 10 ] || nc -z 127.0.0.1 10000; do + sleep $(( NEXT_WAIT_TIME++ )) +done +[ $NEXT_WAIT_TIME -lt 5 ] + +echo "server listening" diff --git a/test/runtests.jl b/test/runtests.jl index 02cbfc4..51a121b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,154 +1,6 @@ using gRPCClient using Random using Test -using Profile - -include("RouteGuideClients/RouteGuideClients.jl") -using .RouteGuideClients - -Base.show(io::IO, location::RouteGuideClients.Point) = print(io, string("[", location.latitude, ", ", location.longitude, "]")) -Base.show(io::IO, feature::RouteGuideClients.Feature) = print(io, string(feature.name, " - ", feature.location)) -Base.show(io::IO, summary::RouteGuideClients.RouteSummary) = print(io, string(summary.point_count, " points, ", summary.feature_count, " features, distance=", summary.distance, ", elapsed_time=", summary.elapsed_time)) -Base.show(io::IO, note::RouteGuideClients.RouteNote) = print(io, string(note.message, " ", note.location)) - -function randomPoint() - latitude = (abs(rand(Int) % 180) - 90) * 1e7 - longitude = (abs(rand(Int) % 360) - 180) * 1e7 - RouteGuideClients.Point(; latitude=latitude, longitude=longitude) -end - -# single request, single response -function test_get_feature(client::RouteGuideBlockingClient) - # existing feature - point = RouteGuideClients.Point(; latitude=409146138, longitude=-746188906) - feature, status_future = RouteGuideClients.GetFeature(client, point) - gRPCCheck(status_future) - @test isa(feature, RouteGuideClients.Feature) - @debug("existing feature", feature) - - # missing feature - point = RouteGuideClients.Point(; latitude=0, longitude=0) - feature, status_future = RouteGuideClients.GetFeature(client, point) - gRPCCheck(status_future) - @test isa(feature, RouteGuideClients.Feature) - @debug("missing feature", feature) -end - -# single request, streaming response -function test_list_features(client::RouteGuideBlockingClient) - @debug("listing features in an area") - rect = RouteGuideClients.Rectangle(; lo=RouteGuideClients.Point(; latitude=400000000, longitude=-750000000), hi=RouteGuideClients.Point(; latitude=420000000, longitude=-730000000)) - features, status_future = RouteGuideClients.ListFeatures(client, rect) - while isopen(features) || isready(features) - try - feature = take!(features) - @debug(feature) - catch ex - (!isa(ex, InvalidStateException) || !fetch(status_future).success) && rethrow(ex) - end - end - gRPCCheck(status_future) - @test isa(features, Channel{RouteGuideClients.Feature}) - @test !isopen(features) -end - -# streaming request, single response -function test_record_route(client::RouteGuideBlockingClient) - @sync begin - point_count = abs(rand(Int) % 100) + 2 - @debug("recording a route", point_count) - points_channel = Channel{RouteGuideClients.Point}(1) - @async begin - for idx in 1:point_count - put!(points_channel, randomPoint()) - end - close(points_channel) - end - route_summary, status_future = RouteGuideClients.RecordRoute(client, points_channel) - gRPCCheck(status_future) - @test isa(route_summary, RouteGuideClients.RouteSummary) - @test !isopen(points_channel) - @debug("route summary: $route_summary") - end -end - -# streaming request, streaming response -function test_route_chat(client::RouteGuideBlockingClient) - @sync begin - notes = RouteGuideClients.RouteNote[ - RouteGuideClients.RouteNote(;location=RouteGuideClients.Point(;latitude=0, longitude=1), message="First message"), - RouteGuideClients.RouteNote(;location=RouteGuideClients.Point(;latitude=0, longitude=2), message="Second message"), - RouteGuideClients.RouteNote(;location=RouteGuideClients.Point(;latitude=0, longitude=3), message="Third message"), - RouteGuideClients.RouteNote(;location=RouteGuideClients.Point(;latitude=0, longitude=1), message="Fourth message"), - RouteGuideClients.RouteNote(;location=RouteGuideClients.Point(;latitude=0, longitude=2), message="Fifth message"), - RouteGuideClients.RouteNote(;location=RouteGuideClients.Point(;latitude=0, longitude=3), message="Sixth message"), - ] - @debug("route chat") - in_channel = Channel{RouteGuideClients.RouteNote}(1) - @async begin - for note in notes - put!(in_channel, note) - end - close(in_channel) - end - out_channel, status_future = RouteGuideClients.RouteChat(client, in_channel) - nreceived = 0 - for note in out_channel - nreceived += 1 - @debug("received note $note") - end - gRPCCheck(status_future) - @test nreceived > 0 - @test isa(out_channel, Channel{RouteGuideClients.RouteNote}) - @test !isopen(out_channel) - @test !isopen(in_channel) - end -end - -function test_exception() - client = RouteGuideBlockingClient("https://localhost:30000"; verbose=false) - point = RouteGuideClients.Point(; latitude=409146138, longitude=-746188906) - feature, status_future = RouteGuideClients.GetFeature(client, point) - @test_throws gRPCException gRPCCheck(status_future) - @test !gRPCCheck(status_future; throw_error=false) -end - -function test_async_get_feature(client::RouteGuideClient) - # existing feature - point = RouteGuideClients.Point(; latitude=409146138, longitude=-746188906) - results = Channel{Any}(1) - RouteGuideClients.GetFeature(client, point, result->put!(results, result)) - feature, status_future = take!(results) - gRPCCheck(status_future) - @test isa(feature, RouteGuideClients.Feature) - @debug("existing feature", feature) -end - -function test_async_client(server_endpoint::String) - client = RouteGuideClient(server_endpoint; verbose=false) - @testset "GetFeature" begin - test_async_get_feature(client) - end -end - -function test_blocking_client(server_endpoint::String) - client = RouteGuideBlockingClient(server_endpoint; verbose=false) - @testset "GetFeature" begin - test_get_feature(client) - end - @testset "ListFeatures" begin - test_list_features(client) - end - @testset "RecordRoute" begin - test_record_route(client) - end - @testset "RouteChat" begin - test_route_chat(client) - end - @testset "ErrorHandling" begin - test_exception() - end -end function test_generate() @testset "codegen" begin @@ -160,16 +12,22 @@ function test_generate() end end -function test_clients(server_endpoint::String) - test_blocking_client(server_endpoint) - test_async_client(server_endpoint) +# e.g.: SSL_CERT_FILE=/path/to/gRPCClient/test/certgen/ca.crt julia runtests.jl https://hostname:10000/ +if isempty(get(ENV, "SSL_CERT_FILE", "")) + ENV["SSL_CERT_FILE"] = joinpath(@__DIR__, "certgen", "ca.crt") end -if length(ARGS) == 1 - @testset "gRPCClient" begin - test_generate() - test_clients(ARGS[1]) - end -else - error("Usage: julia runtests.jl [server_endpoint]") +# switch off host verification for tests +if isempty(get(ENV, "JULIA_NO_VERIFY_HOSTS", "")) + ENV["JULIA_NO_VERIFY_HOSTS"] = "**" end + +server_endpoint = isempty(ARGS) ? "https://$(strip(read(`hostname -f`, String))):10000/" : ARGS[1] +@info("server endpoint: $server_endpoint") + +@testset "gRPCClient" begin + test_generate() + include("test_routeclient.jl") + @info("testing routeclinet...") + test_clients(server_endpoint) +end \ No newline at end of file diff --git a/test/test_routeclient.jl b/test/test_routeclient.jl new file mode 100644 index 0000000..2e08bca --- /dev/null +++ b/test/test_routeclient.jl @@ -0,0 +1,158 @@ +include("RouteGuideClients/RouteGuideClients.jl") +using .RouteGuideClients + +Base.show(io::IO, location::RouteGuideClients.Point) = print(io, string("[", location.latitude, ", ", location.longitude, "]")) +Base.show(io::IO, feature::RouteGuideClients.Feature) = print(io, string(feature.name, " - ", feature.location)) +Base.show(io::IO, summary::RouteGuideClients.RouteSummary) = print(io, string(summary.point_count, " points, ", summary.feature_count, " features, distance=", summary.distance, ", elapsed_time=", summary.elapsed_time)) +Base.show(io::IO, note::RouteGuideClients.RouteNote) = print(io, string(note.message, " ", note.location)) + +function randomPoint() + latitude = (abs(rand(Int) % 180) - 90) * 1e7 + longitude = (abs(rand(Int) % 360) - 180) * 1e7 + RouteGuideClients.Point(; latitude=latitude, longitude=longitude) +end + +# single request, single response +function test_get_feature(client::RouteGuideBlockingClient) + # existing feature + point = RouteGuideClients.Point(; latitude=409146138, longitude=-746188906) + feature, status_future = RouteGuideClients.GetFeature(client, point) + gRPCCheck(status_future) + @test isa(feature, RouteGuideClients.Feature) + @debug("existing feature", feature) + + # missing feature + point = RouteGuideClients.Point(; latitude=0, longitude=0) + feature, status_future = RouteGuideClients.GetFeature(client, point) + gRPCCheck(status_future) + @test isa(feature, RouteGuideClients.Feature) + @debug("missing feature", feature) +end + +# single request, streaming response +function test_list_features(client::RouteGuideBlockingClient) + @debug("listing features in an area") + rect = RouteGuideClients.Rectangle(; lo=RouteGuideClients.Point(; latitude=400000000, longitude=-750000000), hi=RouteGuideClients.Point(; latitude=420000000, longitude=-730000000)) + features, status_future = RouteGuideClients.ListFeatures(client, rect) + while isopen(features) || isready(features) + try + feature = take!(features) + @debug(feature) + catch ex + (!isa(ex, InvalidStateException) || !fetch(status_future).success) && rethrow(ex) + end + end + gRPCCheck(status_future) + @test isa(features, Channel{RouteGuideClients.Feature}) + @test !isopen(features) +end + +# streaming request, single response +function test_record_route(client::RouteGuideBlockingClient) + @sync begin + point_count = abs(rand(Int) % 100) + 2 + @debug("recording a route", point_count) + points_channel = Channel{RouteGuideClients.Point}(1) + @async begin + for idx in 1:point_count + put!(points_channel, randomPoint()) + end + close(points_channel) + end + route_summary, status_future = RouteGuideClients.RecordRoute(client, points_channel) + gRPCCheck(status_future) + @test isa(route_summary, RouteGuideClients.RouteSummary) + @test !isopen(points_channel) + @debug("route summary: $route_summary") + end +end + +# streaming request, streaming response +function test_route_chat(client::RouteGuideBlockingClient) + @sync begin + notes = RouteGuideClients.RouteNote[ + RouteGuideClients.RouteNote(;location=RouteGuideClients.Point(;latitude=0, longitude=1), message="First message"), + RouteGuideClients.RouteNote(;location=RouteGuideClients.Point(;latitude=0, longitude=2), message="Second message"), + RouteGuideClients.RouteNote(;location=RouteGuideClients.Point(;latitude=0, longitude=3), message="Third message"), + RouteGuideClients.RouteNote(;location=RouteGuideClients.Point(;latitude=0, longitude=1), message="Fourth message"), + RouteGuideClients.RouteNote(;location=RouteGuideClients.Point(;latitude=0, longitude=2), message="Fifth message"), + RouteGuideClients.RouteNote(;location=RouteGuideClients.Point(;latitude=0, longitude=3), message="Sixth message"), + ] + @debug("route chat") + in_channel = Channel{RouteGuideClients.RouteNote}(1) + @async begin + for note in notes + put!(in_channel, note) + end + close(in_channel) + end + out_channel, status_future = RouteGuideClients.RouteChat(client, in_channel) + nreceived = 0 + for note in out_channel + nreceived += 1 + @debug("received note $note") + end + gRPCCheck(status_future) + @test nreceived > 0 + @test isa(out_channel, Channel{RouteGuideClients.RouteNote}) + @test !isopen(out_channel) + @test !isopen(in_channel) + end +end + +function test_exception() + client = RouteGuideBlockingClient("https://localhost:30000"; verbose=false) + point = RouteGuideClients.Point(; latitude=409146138, longitude=-746188906) + feature, status_future = RouteGuideClients.GetFeature(client, point) + @test_throws gRPCException gRPCCheck(status_future) + @test !gRPCCheck(status_future; throw_error=false) +end + +function test_async_get_feature(client::RouteGuideClient) + # existing feature + point = RouteGuideClients.Point(; latitude=409146138, longitude=-746188906) + results = Channel{Any}(1) + RouteGuideClients.GetFeature(client, point, result->put!(results, result)) + feature, status_future = take!(results) + gRPCCheck(status_future) + @test isa(feature, RouteGuideClients.Feature) + @debug("existing feature", feature) +end + +function test_async_client(server_endpoint::String) + client = RouteGuideClient(server_endpoint; verbose=false) + @testset "GetFeature" begin + test_async_get_feature(client) + end +end + +function test_blocking_client(server_endpoint::String) + client = RouteGuideBlockingClient(server_endpoint; verbose=false) + @info("testing GetFeature") + @testset "GetFeature" begin + test_get_feature(client) + end + @info("testing ListFeatures") + @testset "ListFeatures" begin + test_list_features(client) + end + @info("testing RecordRoute") + @testset "RecordRoute" begin + test_record_route(client) + end + @info("testing RouteChat") + @testset "RouteChat" begin + test_route_chat(client) + end + @info("testing ErrorHandling") + @testset "ErrorHandling" begin + test_exception() + end +end + +function test_clients(server_endpoint::String) + @info("testing blocking client") + test_blocking_client(server_endpoint) + @info("testing async client") + test_async_client(server_endpoint) +end \ No newline at end of file