Skip to content

Commit e6c67fd

Browse files
authored
add grpcurl integration graph traversal tests (#71)
* add grpcurl integration graph traversal tests * add grpcurl to CI setup * seitch grpcurl from classic to edge snap * add snap bin to path * try again * also install grpcurl for deps unlocked step * add comments * grab grpcurl release from github instead of using snap
1 parent 9fba85d commit e6c67fd

4 files changed

Lines changed: 208 additions & 1 deletion

File tree

.github/workflows/ci.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ jobs:
4242
steps:
4343
- uses: actions/checkout@v4
4444

45+
- name: Install grpcurl
46+
run: |
47+
GRPCURL_VERSION="1.9.3"
48+
curl -sSL https://github.com/fullstorydev/grpcurl/releases/download/v1.9.3/grpcurl_${GRPCURL_VERSION}_linux_x86_64.tar.gz | tar -xvz
49+
mv grpcurl /usr/local/bin/grpcurl
50+
grpcurl --version
51+
4552
- name: Set up Elixir
4653
id: beam
4754
uses: erlef/setup-beam@v1
@@ -134,6 +141,13 @@ jobs:
134141
steps:
135142
- uses: actions/checkout@v4
136143

144+
- name: Install grpcurl
145+
run: |
146+
GRPCURL_VERSION="1.9.3"
147+
curl -sSL https://github.com/fullstorydev/grpcurl/releases/download/v1.9.3/grpcurl_${GRPCURL_VERSION}_linux_x86_64.tar.gz | tar -xvz
148+
mv grpcurl /usr/local/bin/grpcurl
149+
grpcurl --version
150+
137151
- name: Set up Elixir
138152
id: beam
139153
uses: erlef/setup-beam@v1

test/integration/v1_reflection_test.exs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,4 +436,48 @@ defmodule GrpcReflection.V1ReflectionTest do
436436
{:list_services, ""}
437437
]
438438
end
439+
440+
test "reflection graph is traversable using grpcurl", ctx do
441+
ops = GrpcReflection.TestClient.grpcurl_service(ctx)
442+
443+
assert ops == [
444+
{:call, "grpc.reflection.v1.ServerReflection.ServerReflectionInfo"},
445+
{:call, "grpc.reflection.v1alpha.ServerReflection.ServerReflectionInfo"},
446+
{:call, "helloworld.Greeter.SayHello"},
447+
{:call, "testserviceV2.TestService.CallFunction"},
448+
{:call, "testserviceV3.TestService.CallFunction"},
449+
{:service, "grpc.reflection.v1.ServerReflection"},
450+
{:service, "grpc.reflection.v1alpha.ServerReflection"},
451+
{:service, "helloworld.Greeter"},
452+
{:service, "testserviceV2.TestService"},
453+
{:service, "testserviceV3.TestService"},
454+
{:type, ".google.protobuf.Any"},
455+
{:type, ".google.protobuf.StringValue"},
456+
{:type, ".google.protobuf.Timestamp"},
457+
{:type, ".grpc.reflection.v1.ErrorResponse"},
458+
{:type, ".grpc.reflection.v1.ExtensionNumberResponse"},
459+
{:type, ".grpc.reflection.v1.ExtensionRequest"},
460+
{:type, ".grpc.reflection.v1.FileDescriptorResponse"},
461+
{:type, ".grpc.reflection.v1.ListServiceResponse"},
462+
{:type, ".grpc.reflection.v1.ServerReflectionRequest"},
463+
{:type, ".grpc.reflection.v1.ServerReflectionResponse"},
464+
{:type, ".grpc.reflection.v1.ServiceResponse"},
465+
{:type, ".grpc.reflection.v1alpha.ErrorResponse"},
466+
{:type, ".grpc.reflection.v1alpha.ExtensionNumberResponse"},
467+
{:type, ".grpc.reflection.v1alpha.ExtensionRequest"},
468+
{:type, ".grpc.reflection.v1alpha.FileDescriptorResponse"},
469+
{:type, ".grpc.reflection.v1alpha.ListServiceResponse"},
470+
{:type, ".grpc.reflection.v1alpha.ServerReflectionRequest"},
471+
{:type, ".grpc.reflection.v1alpha.ServerReflectionResponse"},
472+
{:type, ".grpc.reflection.v1alpha.ServiceResponse"},
473+
{:type, ".helloworld.HelloReply"},
474+
{:type, ".helloworld.HelloRequest"},
475+
{:type, ".testserviceV2.Enum"},
476+
{:type, ".testserviceV2.TestReply"},
477+
{:type, ".testserviceV2.TestRequest"},
478+
{:type, ".testserviceV3.Enum"},
479+
{:type, ".testserviceV3.TestReply"},
480+
{:type, ".testserviceV3.TestRequest"}
481+
]
482+
end
439483
end

test/integration/v1alpha_reflection_test.exs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,4 +439,48 @@ defmodule GrpcReflection.V1alphaReflectionTest do
439439
{:list_services, ""}
440440
]
441441
end
442+
443+
test "reflection graph is traversable using grpcurl", ctx do
444+
ops = GrpcReflection.TestClient.grpcurl_service(ctx)
445+
446+
assert ops == [
447+
{:call, "grpc.reflection.v1.ServerReflection.ServerReflectionInfo"},
448+
{:call, "grpc.reflection.v1alpha.ServerReflection.ServerReflectionInfo"},
449+
{:call, "helloworld.Greeter.SayHello"},
450+
{:call, "testserviceV2.TestService.CallFunction"},
451+
{:call, "testserviceV3.TestService.CallFunction"},
452+
{:service, "grpc.reflection.v1.ServerReflection"},
453+
{:service, "grpc.reflection.v1alpha.ServerReflection"},
454+
{:service, "helloworld.Greeter"},
455+
{:service, "testserviceV2.TestService"},
456+
{:service, "testserviceV3.TestService"},
457+
{:type, ".google.protobuf.Any"},
458+
{:type, ".google.protobuf.StringValue"},
459+
{:type, ".google.protobuf.Timestamp"},
460+
{:type, ".grpc.reflection.v1.ErrorResponse"},
461+
{:type, ".grpc.reflection.v1.ExtensionNumberResponse"},
462+
{:type, ".grpc.reflection.v1.ExtensionRequest"},
463+
{:type, ".grpc.reflection.v1.FileDescriptorResponse"},
464+
{:type, ".grpc.reflection.v1.ListServiceResponse"},
465+
{:type, ".grpc.reflection.v1.ServerReflectionRequest"},
466+
{:type, ".grpc.reflection.v1.ServerReflectionResponse"},
467+
{:type, ".grpc.reflection.v1.ServiceResponse"},
468+
{:type, ".grpc.reflection.v1alpha.ErrorResponse"},
469+
{:type, ".grpc.reflection.v1alpha.ExtensionNumberResponse"},
470+
{:type, ".grpc.reflection.v1alpha.ExtensionRequest"},
471+
{:type, ".grpc.reflection.v1alpha.FileDescriptorResponse"},
472+
{:type, ".grpc.reflection.v1alpha.ListServiceResponse"},
473+
{:type, ".grpc.reflection.v1alpha.ServerReflectionRequest"},
474+
{:type, ".grpc.reflection.v1alpha.ServerReflectionResponse"},
475+
{:type, ".grpc.reflection.v1alpha.ServiceResponse"},
476+
{:type, ".helloworld.HelloReply"},
477+
{:type, ".helloworld.HelloRequest"},
478+
{:type, ".testserviceV2.Enum"},
479+
{:type, ".testserviceV2.TestReply"},
480+
{:type, ".testserviceV2.TestRequest"},
481+
{:type, ".testserviceV3.Enum"},
482+
{:type, ".testserviceV3.TestReply"},
483+
{:type, ".testserviceV3.TestRequest"}
484+
]
485+
end
442486
end

test/support/client.ex

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ defmodule GrpcReflection.TestClient do
2828
:v1alpha -> GrpcReflection.TestEndpoint.V1AlphaServer.Stub
2929
end
3030

31-
%{channel: channel, req: req, stub: stub, version: unquote(version)}
31+
%{channel: channel, req: req, stub: stub, version: unquote(version), host: host}
3232
end
3333
end
3434
end
@@ -148,4 +148,109 @@ defmodule GrpcReflection.TestClient do
148148
extendee_commands ++ nested_commands
149149
end)
150150
end
151+
152+
def grpcurl_service(ctx) do
153+
ctx
154+
|> grpcurl_list_services()
155+
|> Stream.unfold(fn
156+
[] ->
157+
nil
158+
159+
[{:service, service} | rest] = term ->
160+
commands = grpcurl_describe_service(ctx, service)
161+
{term, commands ++ rest}
162+
163+
[{:call, call} | rest] = term ->
164+
commands = grpcurl_describe_call(ctx, call)
165+
{term, commands ++ rest}
166+
167+
[{:type, type} | rest] = term ->
168+
commands = grpcurl_describe_type(ctx, type)
169+
{term, commands ++ rest}
170+
end)
171+
|> Enum.to_list()
172+
|> List.flatten()
173+
|> Enum.uniq()
174+
|> Enum.sort()
175+
end
176+
177+
defp grpcurl_list_services(%{host: host}) do
178+
{result, 0} = System.cmd("grpcurl", ["-v", "-plaintext", host, "list"])
179+
180+
result
181+
|> String.split("\n")
182+
|> Enum.reject(&(&1 == ""))
183+
|> Enum.map(&{:service, &1})
184+
end
185+
186+
defp grpcurl_describe_service(%{host: host}, service) do
187+
{result, 0} = System.cmd("grpcurl", ["-v", "-plaintext", host, "list", service])
188+
189+
result
190+
|> String.split("\n")
191+
|> Enum.reject(&(&1 == ""))
192+
|> Enum.map(&{:call, &1})
193+
end
194+
195+
defp grpcurl_describe_call(%{host: host}, call) do
196+
{result, 0} = System.cmd("grpcurl", ["-v", "-plaintext", host, "describe", call])
197+
198+
~r/\((?: stream)? (?<symbol>[a-zA-Z0-9.]+) \)/
199+
|> Regex.scan(result, capture: ["symbol"])
200+
|> List.flatten()
201+
|> Enum.map(&{:type, &1})
202+
end
203+
204+
defp grpcurl_describe_type(%{host: host}, type) do
205+
{result, 0} = System.cmd("grpcurl", ["-v", "-plaintext", host, "describe", type])
206+
207+
# we are grabbing the referenced types to we can fetch those too
208+
# but some of them might be defined inside this file
209+
# so we have to identify those and filter them back out
210+
211+
inline_declared_symbols =
212+
~r/(?:message (?<name>\w+) {)|(?<close>})|(?<open>{)/
213+
|> Regex.scan(result, capture: :all_but_first)
214+
|> List.flatten()
215+
|> Enum.reject(&(&1 == ""))
216+
# replace first declaration with base type to get full names
217+
|> then(fn [_base | rest] -> [type | rest] end)
218+
|> remove_matched_parens()
219+
|> Enum.reduce({[], []}, fn token, {path, inline_types} ->
220+
case token do
221+
"}" ->
222+
[_ | path] = path
223+
# End of current message — pop from path
224+
{path, inline_types}
225+
226+
name ->
227+
path = [name | path]
228+
name = path |> Enum.reverse() |> Enum.join(".")
229+
{path, [name | inline_types]}
230+
end
231+
end)
232+
|> elem(1)
233+
234+
# now we can grab all the references, then reject the nested declarations
235+
~r/ (?<symbol>\.[a-z]+[a-zA-Z0-9.]+) /
236+
|> Regex.scan(result, capture: ["symbol"])
237+
|> List.flatten()
238+
|> Enum.reject(&Enum.member?(inline_declared_symbols, &1))
239+
|> Enum.map(&{:type, &1})
240+
end
241+
242+
# if we only match `message XYZ {` and `}`, we get extra `}` tokens from things like the
243+
# oneof declarations. If we also match non-message `{` we can eliminate these intermeidate
244+
# paren-blocks, which we do here for our needs
245+
defp remove_matched_parens(["{", "}" | rest]) do
246+
remove_matched_parens(rest)
247+
end
248+
249+
defp remove_matched_parens([item | rest]) do
250+
[item | remove_matched_parens(rest)]
251+
end
252+
253+
defp remove_matched_parens(rest) do
254+
rest
255+
end
151256
end

0 commit comments

Comments
 (0)