From 576c513519433f0c9f08d609bdb20241251cc305 Mon Sep 17 00:00:00 2001 From: mjheilmann Date: Tue, 30 Dec 2025 22:38:44 -0500 Subject: [PATCH 1/2] update testing --- lib/grpc_reflection/service/builder/util.ex | 3 + priv/protos/recursive_message.proto | 15 + test/integration/v1_reflection_test.exs | 358 +++++++++++++++++-- test/integration/v1alpha_reflection_test.exs | 358 +++++++++++++++++-- test/service/builder_test.exs | 17 + test/support/protos/recursive_message.pb.ex | 120 +++++++ 6 files changed, 809 insertions(+), 62 deletions(-) create mode 100644 priv/protos/recursive_message.proto create mode 100644 test/support/protos/recursive_message.pb.ex diff --git a/lib/grpc_reflection/service/builder/util.ex b/lib/grpc_reflection/service/builder/util.ex index 89f0740..7236571 100644 --- a/lib/grpc_reflection/service/builder/util.ex +++ b/lib/grpc_reflection/service/builder/util.ex @@ -163,6 +163,9 @@ defmodule GrpcReflection.Service.Builder.Util do [] end + # in gRPC, the leading "." signifies it is a FQDN + # we trim it and assume everything is a FQDN + # it works so far, but there may be corner cases def trim_symbol("." <> symbol), do: symbol def trim_symbol(symbol), do: symbol end diff --git a/priv/protos/recursive_message.proto b/priv/protos/recursive_message.proto new file mode 100644 index 0000000..d97b4e4 --- /dev/null +++ b/priv/protos/recursive_message.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package recursive_message; + +service Service { + rpc call (Request) returns (Reply) {} +} + +message Request { + Reply reply = 1; +} + +message Reply { + Request request = 1; +} \ No newline at end of file diff --git a/test/integration/v1_reflection_test.exs b/test/integration/v1_reflection_test.exs index ea5d94f..fd6e7af 100644 --- a/test/integration/v1_reflection_test.exs +++ b/test/integration/v1_reflection_test.exs @@ -25,67 +25,65 @@ defmodule GrpcReflection.V1ReflectionTest do assert {:error, _} = run_request(message, ctx) end - describe "listing services" do - test "listing services", ctx do - message = {:list_services, ""} - assert {:ok, %{service: service_list}} = run_request(message, ctx) - names = Enum.map(service_list, &Map.get(&1, :name)) - - assert names == [ - "helloworld.Greeter", - "testserviceV2.TestService", - "testserviceV3.TestService", - "grpc.reflection.v1.ServerReflection", - "grpc.reflection.v1alpha.ServerReflection" - ] - end + test "listing services", ctx do + message = {:list_services, ""} + assert {:ok, %{service: service_list}} = run_request(message, ctx) + names = Enum.map(service_list, &Map.get(&1, :name)) + + assert names == [ + "helloworld.Greeter", + "testserviceV2.TestService", + "testserviceV3.TestService", + "grpc.reflection.v1.ServerReflection", + "grpc.reflection.v1alpha.ServerReflection" + ] end - describe "describe by symbol" do - test "unknown symbol is rejected", ctx do + describe "symbol queries" do + test "ushould reject nknown symbol", ctx do message = {:file_containing_symbol, "other.Rejecter"} assert {:error, _} = run_request(message, ctx) end - test "listing methods on our service", ctx do + test "should list methods on our service", ctx do message = {:file_containing_symbol, "helloworld.Greeter"} assert {:ok, response} = run_request(message, ctx) assert_response(response) end - test "describing a method returns the service", ctx do + test "should return a method on the service", ctx do message = {:file_containing_symbol, "helloworld.Greeter.SayHello"} assert {:ok, response} = run_request(message, ctx) assert_response(response) end - test "describing an invalid method returns not found", ctx do + test "should return not found for an invalid method", ctx do # SayHellp is not a method on the service message = {:file_containing_symbol, "helloworld.Greeter.SayHellp"} assert {:error, _} = run_request(message, ctx) end - test "describing a root type returns the type", ctx do + test "should resolve a type", ctx do message = {:file_containing_symbol, "helloworld.HelloRequest"} assert {:ok, response} = run_request(message, ctx) assert_response(response) end - test "describing a nested type returns the root type", ctx do + test "should return the containing file for a nested message", ctx do message = {:file_containing_symbol, "testserviceV3.TestRequest.Payload"} assert {:ok, response} = run_request(message, ctx) assert response.name == "testserviceV3.TestRequest.proto" end - test "type with leading period still resolves", ctx do + test "should resolve types with the leading period", ctx do message = {:file_containing_symbol, ".helloworld.HelloRequest"} assert {:ok, response} = run_request(message, ctx) assert_response(response) end end - describe "filename traversal" do - test "listing methods on our service", ctx do + describe "filename queries" do + test "should list methods on our service", ctx do message = {:file_containing_symbol, "helloworld.Greeter"} assert {:ok, response} = run_request(message, ctx) assert_response(response) @@ -97,13 +95,13 @@ defmodule GrpcReflection.V1ReflectionTest do ] end - test "reject filename that doesn't match a reflection module", ctx do + test "should reject an unrecognized filename", ctx do filename = "does.not.exist.proto" message = {:file_by_filename, filename} assert {:error, _} = run_request(message, ctx) end - test "get replytype by filename", ctx do + test "should resolve by filename", ctx do filename = "helloworld.HelloReply.proto" message = {:file_by_filename, filename} assert {:ok, response} = run_request(message, ctx) @@ -135,7 +133,7 @@ defmodule GrpcReflection.V1ReflectionTest do ] = response.message_type end - test "get external by filename", ctx do + test "should resolve external dependency types", ctx do filename = "google.protobuf.Timestamp.proto" message = {:file_by_filename, filename} assert {:ok, response} = run_request(message, ctx) @@ -166,7 +164,7 @@ defmodule GrpcReflection.V1ReflectionTest do ] = response.message_type end - test "ensures file descriptor dependencies are unique", ctx do + test "should not duplicate dependencies in the file descriptor", ctx do filename = "testserviceV3.TestReply.proto" message = {:file_by_filename, filename} assert {:ok, response} = run_request(message, ctx) @@ -179,7 +177,7 @@ defmodule GrpcReflection.V1ReflectionTest do ] end - test "ensure exclusion of nested types in file descriptor dependencies", ctx do + test "should exclude nested types from dependenciy list", ctx do filename = "testserviceV3.TestRequest.proto" message = {:file_by_filename, filename} assert {:ok, response} = run_request(message, ctx) @@ -195,7 +193,7 @@ defmodule GrpcReflection.V1ReflectionTest do end describe "proto2 extensions" do - test "get all extension numbers by type", ctx do + test "should get all extension numbers by type", ctx do type = "testserviceV2.TestRequest" message = {:all_extension_numbers_of_type, type} assert {:ok, response} = run_request(message, ctx) @@ -203,7 +201,7 @@ defmodule GrpcReflection.V1ReflectionTest do assert response.extension_number == [10, 11] end - test "get extension descriptor file by extendee", ctx do + test "should get extension descriptor file by extendee", ctx do extendee = "testserviceV2.TestRequest" message = @@ -258,4 +256,302 @@ defmodule GrpcReflection.V1ReflectionTest do ] end end + + test "reflection graph is traversable", ctx do + ops = + Stream.unfold([{:list_services, ""}], fn + [] -> + nil + + [{:list_services, ""} = message | rest] = term -> + # add commands to get the files for the listed services + {:ok, %{service: service_list}} = run_request(message, ctx) + + commands = + Enum.map(service_list, fn %{name: service_name} -> + {:file_containing_symbol, service_name} + end) + + {term, commands ++ rest} + + [{:file_by_filename, _} = message | rest] = term -> + message + |> run_request(ctx) + |> case do + {:ok, descriptor} -> + commands = links_from_descriptor(descriptor) + {term, commands ++ rest} + end + + [{:file_containing_extension, _} = message | rest] = term -> + message + |> run_request(ctx) + |> case do + {:ok, _descriptor} -> + # extensions depend on the base type + # if we load the dependencies and feed it into the unfold action + # we get stuck in a loop + {term, rest} + + {:error, %{error_message: "extension not found"}} -> + {term, rest} + end + + [{:file_containing_symbol, _} = message | rest] = term -> + # get the file containing the symbol, and add commands to get the dependencies + message + |> run_request(ctx) + |> case do + {:ok, descriptor} -> + commands = links_from_descriptor(descriptor) + {term, commands ++ rest} + end + end) + |> Enum.to_list() + |> List.flatten() + |> Enum.uniq() + |> Enum.sort() + + assert ops == [ + {:file_by_filename, "google.protobuf.Any.proto"}, + {:file_by_filename, "google.protobuf.StringValue.proto"}, + {:file_by_filename, "google.protobuf.Timestamp.proto"}, + {:file_by_filename, "grpc.reflection.v1.ErrorResponse.proto"}, + {:file_by_filename, "grpc.reflection.v1.ExtensionNumberResponse.proto"}, + {:file_by_filename, "grpc.reflection.v1.ExtensionRequest.proto"}, + {:file_by_filename, "grpc.reflection.v1.FileDescriptorResponse.proto"}, + {:file_by_filename, "grpc.reflection.v1.ListServiceResponse.proto"}, + {:file_by_filename, "grpc.reflection.v1.ServerReflectionRequest.proto"}, + {:file_by_filename, "grpc.reflection.v1.ServerReflectionResponse.proto"}, + {:file_by_filename, "grpc.reflection.v1.ServiceResponse.proto"}, + {:file_by_filename, "grpc.reflection.v1alpha.ErrorResponse.proto"}, + {:file_by_filename, "grpc.reflection.v1alpha.ExtensionNumberResponse.proto"}, + {:file_by_filename, "grpc.reflection.v1alpha.ExtensionRequest.proto"}, + {:file_by_filename, "grpc.reflection.v1alpha.FileDescriptorResponse.proto"}, + {:file_by_filename, "grpc.reflection.v1alpha.ListServiceResponse.proto"}, + {:file_by_filename, "grpc.reflection.v1alpha.ServerReflectionRequest.proto"}, + {:file_by_filename, "grpc.reflection.v1alpha.ServerReflectionResponse.proto"}, + {:file_by_filename, "grpc.reflection.v1alpha.ServiceResponse.proto"}, + {:file_by_filename, "helloworld.HelloReply.proto"}, + {:file_by_filename, "helloworld.HelloRequest.proto"}, + {:file_by_filename, "testserviceV2.Enum.proto"}, + {:file_by_filename, "testserviceV2.TestReply.proto"}, + {:file_by_filename, "testserviceV2.TestRequest.proto"}, + {:file_by_filename, "testserviceV3.Enum.proto"}, + {:file_by_filename, "testserviceV3.TestReply.proto"}, + {:file_by_filename, "testserviceV3.TestRequest.proto"}, + {:file_containing_extension, + %Grpc.Reflection.V1.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest", + extension_number: 10, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest", + extension_number: 11, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest", + extension_number: 12, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest", + extension_number: 13, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest", + extension_number: 14, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest", + extension_number: 15, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest", + extension_number: 16, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest", + extension_number: 17, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest", + extension_number: 18, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest", + extension_number: 19, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest", + extension_number: 20, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest", + extension_number: 21, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest.GEntry", + extension_number: 10, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest.GEntry", + extension_number: 11, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest.GEntry", + extension_number: 12, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest.GEntry", + extension_number: 13, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest.GEntry", + extension_number: 14, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest.GEntry", + extension_number: 15, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest.GEntry", + extension_number: 16, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest.GEntry", + extension_number: 17, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest.GEntry", + extension_number: 18, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest.GEntry", + extension_number: 19, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest.GEntry", + extension_number: 20, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest.GEntry", + extension_number: 21, + __unknown_fields__: [] + }}, + {:file_containing_symbol, ".grpc.reflection.v1.ServerReflectionRequest"}, + {:file_containing_symbol, ".grpc.reflection.v1.ServerReflectionResponse"}, + {:file_containing_symbol, ".grpc.reflection.v1alpha.ServerReflectionRequest"}, + {:file_containing_symbol, ".grpc.reflection.v1alpha.ServerReflectionResponse"}, + {:file_containing_symbol, ".helloworld.HelloReply"}, + {:file_containing_symbol, ".helloworld.HelloRequest"}, + {:file_containing_symbol, ".testserviceV2.TestReply"}, + {:file_containing_symbol, ".testserviceV2.TestRequest"}, + {:file_containing_symbol, ".testserviceV3.TestReply"}, + {:file_containing_symbol, ".testserviceV3.TestRequest"}, + {:file_containing_symbol, "grpc.reflection.v1.ServerReflection"}, + {:file_containing_symbol, "grpc.reflection.v1alpha.ServerReflection"}, + {:file_containing_symbol, "helloworld.Greeter"}, + {:file_containing_symbol, "testserviceV2.TestService"}, + {:file_containing_symbol, "testserviceV3.TestService"}, + {:list_services, ""} + ] + end + + defp links_from_descriptor(%Google.Protobuf.FileDescriptorProto{} = proto) do + file_dependencies(proto) ++ + service_dependencies(proto) ++ + extension_dependencies(proto) + end + + defp file_dependencies(%Google.Protobuf.FileDescriptorProto{dependency: dependencies}) do + Enum.map(dependencies, fn dep_file -> + {:file_by_filename, dep_file} + end) + end + + defp service_dependencies(%Google.Protobuf.FileDescriptorProto{service: services}) do + Enum.flat_map(services, fn %{method: methods} -> + Enum.flat_map(methods, fn + %{input_type: input, output_type: output} -> + [{:file_containing_symbol, input}, {:file_containing_symbol, output}] + end) + end) + end + + defp extension_dependencies(%Google.Protobuf.FileDescriptorProto{ + package: package, + message_type: types + }) do + gen_range = fn + extendee, ranges -> + ranges + |> Enum.flat_map(fn %{start: start, end: finish} -> start..finish//1 end) + |> Enum.map(fn num -> + {:file_containing_extension, + %Grpc.Reflection.V1.ExtensionRequest{ + containing_type: extendee, + extension_number: num + }} + end) + end + + Enum.flat_map(types, fn type -> + extendee = package <> "." <> type.name + extendee_commands = gen_range.(extendee, type.extension_range) + + nested_commands = + Enum.flat_map(type.nested_type, fn nested_type -> + extendee = extendee <> "." <> nested_type.name + gen_range.(extendee, type.extension_range) + end) + + extendee_commands ++ nested_commands + end) + end end diff --git a/test/integration/v1alpha_reflection_test.exs b/test/integration/v1alpha_reflection_test.exs index 0bd5092..c340899 100644 --- a/test/integration/v1alpha_reflection_test.exs +++ b/test/integration/v1alpha_reflection_test.exs @@ -25,67 +25,65 @@ defmodule GrpcReflection.V1alphaReflectionTest do assert {:error, _} = run_request(message, ctx) end - describe "listing services" do - test "listing services", ctx do - message = {:list_services, ""} - assert {:ok, %{service: service_list}} = run_request(message, ctx) - names = Enum.map(service_list, &Map.get(&1, :name)) - - assert names == [ - "helloworld.Greeter", - "testserviceV2.TestService", - "testserviceV3.TestService", - "grpc.reflection.v1.ServerReflection", - "grpc.reflection.v1alpha.ServerReflection" - ] - end + test "listing services", ctx do + message = {:list_services, ""} + assert {:ok, %{service: service_list}} = run_request(message, ctx) + names = Enum.map(service_list, &Map.get(&1, :name)) + + assert names == [ + "helloworld.Greeter", + "testserviceV2.TestService", + "testserviceV3.TestService", + "grpc.reflection.v1.ServerReflection", + "grpc.reflection.v1alpha.ServerReflection" + ] end - describe "describe by symbol" do - test "unknown symbol is rejected", ctx do + describe "symbol queries" do + test "should reject unknown symbol", ctx do message = {:file_containing_symbol, "other.Rejecter"} assert {:error, _} = run_request(message, ctx) end - test "listing methods on our service", ctx do + test "should list methods on our service", ctx do message = {:file_containing_symbol, "helloworld.Greeter"} assert {:ok, response} = run_request(message, ctx) assert_response(response) end - test "describing a method returns the service", ctx do + test "should resolve the service when describing a method", ctx do message = {:file_containing_symbol, "helloworld.Greeter.SayHello"} assert {:ok, response} = run_request(message, ctx) assert_response(response) end - test "describing an invalid method returns not found", ctx do + test "should return not found for an invalid method", ctx do # SayHellp is not a method on the service message = {:file_containing_symbol, "helloworld.Greeter.SayHellp"} assert {:error, _} = run_request(message, ctx) end - test "describing a root type returns the type", ctx do + test "should return the type when it is the root typy", ctx do message = {:file_containing_symbol, "helloworld.HelloRequest"} assert {:ok, response} = run_request(message, ctx) assert_response(response) end - test "describing a nested type returns the root type", ctx do + test "should return the containing type for a query on a nested type", ctx do message = {:file_containing_symbol, "testserviceV3.TestRequest.Payload"} assert {:ok, response} = run_request(message, ctx) assert response.name == "testserviceV3.TestRequest.proto" end - test "type with leading period still resolves", ctx do + test "should also resove with leading period", ctx do message = {:file_containing_symbol, ".helloworld.HelloRequest"} assert {:ok, response} = run_request(message, ctx) assert_response(response) end end - describe "filename traversal" do - test "listing methods on our service", ctx do + describe "filename queries" do + test "should list methods on our service", ctx do message = {:file_containing_symbol, "helloworld.Greeter"} assert {:ok, response} = run_request(message, ctx) assert_response(response) @@ -97,13 +95,13 @@ defmodule GrpcReflection.V1alphaReflectionTest do ] end - test "reject filename that doesn't match a reflection module", ctx do + test "should reject a filename that isn't recognized", ctx do filename = "does.not.exist.proto" message = {:file_by_filename, filename} assert {:error, _} = run_request(message, ctx) end - test "get replytype by filename", ctx do + test "gshould resolve by filename", ctx do filename = "helloworld.HelloReply.proto" message = {:file_by_filename, filename} assert {:ok, response} = run_request(message, ctx) @@ -135,7 +133,7 @@ defmodule GrpcReflection.V1alphaReflectionTest do ] = response.message_type end - test "get external by filename", ctx do + test "should resolve third party messages by filename", ctx do filename = "google.protobuf.Timestamp.proto" message = {:file_by_filename, filename} assert {:ok, response} = run_request(message, ctx) @@ -166,7 +164,7 @@ defmodule GrpcReflection.V1alphaReflectionTest do ] = response.message_type end - test "ensures file descriptor dependencies are unique", ctx do + test "should not duplicate dependencies", ctx do filename = "testserviceV3.TestReply.proto" message = {:file_by_filename, filename} assert {:ok, response} = run_request(message, ctx) @@ -179,7 +177,7 @@ defmodule GrpcReflection.V1alphaReflectionTest do ] end - test "ensure exclusion of nested types in file descriptor dependencies", ctx do + test "should not treat a nested type as a dependency", ctx do filename = "testserviceV3.TestRequest.proto" message = {:file_by_filename, filename} assert {:ok, response} = run_request(message, ctx) @@ -195,7 +193,7 @@ defmodule GrpcReflection.V1alphaReflectionTest do end describe "proto2 extensions" do - test "get all extension numbers by type", ctx do + test "should get all extension numbers by type", ctx do type = "testserviceV2.TestRequest" message = {:all_extension_numbers_of_type, type} assert {:ok, response} = run_request(message, ctx) @@ -203,7 +201,7 @@ defmodule GrpcReflection.V1alphaReflectionTest do assert response.extension_number == [10, 11] end - test "get extension descriptor file by extendee", ctx do + test "should get extension descriptor file by extendee", ctx do extendee = "testserviceV2.TestRequest" message = @@ -261,4 +259,302 @@ defmodule GrpcReflection.V1alphaReflectionTest do ] end end + + test "reflection graph is traversable", ctx do + ops = + Stream.unfold([{:list_services, ""}], fn + [] -> + nil + + [{:list_services, ""} = message | rest] = term -> + # add commands to get the files for the listed services + {:ok, %{service: service_list}} = run_request(message, ctx) + + commands = + Enum.map(service_list, fn %{name: service_name} -> + {:file_containing_symbol, service_name} + end) + + {term, commands ++ rest} + + [{:file_by_filename, _} = message | rest] = term -> + message + |> run_request(ctx) + |> case do + {:ok, descriptor} -> + commands = links_from_descriptor(descriptor) + {term, commands ++ rest} + end + + [{:file_containing_extension, _} = message | rest] = term -> + message + |> run_request(ctx) + |> case do + {:ok, _descriptor} -> + # extensions depend on the base type + # if we load the dependencies and feed it into the unfold action + # we get stuck in a loop + {term, rest} + + {:error, %{error_message: "extension not found"}} -> + {term, rest} + end + + [{:file_containing_symbol, _} = message | rest] = term -> + # get the file containing the symbol, and add commands to get the dependencies + message + |> run_request(ctx) + |> case do + {:ok, descriptor} -> + commands = links_from_descriptor(descriptor) + {term, commands ++ rest} + end + end) + |> Enum.to_list() + |> List.flatten() + |> Enum.uniq() + |> Enum.sort() + + assert ops == [ + {:file_by_filename, "google.protobuf.Any.proto"}, + {:file_by_filename, "google.protobuf.StringValue.proto"}, + {:file_by_filename, "google.protobuf.Timestamp.proto"}, + {:file_by_filename, "grpc.reflection.v1.ErrorResponse.proto"}, + {:file_by_filename, "grpc.reflection.v1.ExtensionNumberResponse.proto"}, + {:file_by_filename, "grpc.reflection.v1.ExtensionRequest.proto"}, + {:file_by_filename, "grpc.reflection.v1.FileDescriptorResponse.proto"}, + {:file_by_filename, "grpc.reflection.v1.ListServiceResponse.proto"}, + {:file_by_filename, "grpc.reflection.v1.ServerReflectionRequest.proto"}, + {:file_by_filename, "grpc.reflection.v1.ServerReflectionResponse.proto"}, + {:file_by_filename, "grpc.reflection.v1.ServiceResponse.proto"}, + {:file_by_filename, "grpc.reflection.v1alpha.ErrorResponse.proto"}, + {:file_by_filename, "grpc.reflection.v1alpha.ExtensionNumberResponse.proto"}, + {:file_by_filename, "grpc.reflection.v1alpha.ExtensionRequest.proto"}, + {:file_by_filename, "grpc.reflection.v1alpha.FileDescriptorResponse.proto"}, + {:file_by_filename, "grpc.reflection.v1alpha.ListServiceResponse.proto"}, + {:file_by_filename, "grpc.reflection.v1alpha.ServerReflectionRequest.proto"}, + {:file_by_filename, "grpc.reflection.v1alpha.ServerReflectionResponse.proto"}, + {:file_by_filename, "grpc.reflection.v1alpha.ServiceResponse.proto"}, + {:file_by_filename, "helloworld.HelloReply.proto"}, + {:file_by_filename, "helloworld.HelloRequest.proto"}, + {:file_by_filename, "testserviceV2.Enum.proto"}, + {:file_by_filename, "testserviceV2.TestReply.proto"}, + {:file_by_filename, "testserviceV2.TestRequest.proto"}, + {:file_by_filename, "testserviceV3.Enum.proto"}, + {:file_by_filename, "testserviceV3.TestReply.proto"}, + {:file_by_filename, "testserviceV3.TestRequest.proto"}, + {:file_containing_extension, + %Grpc.Reflection.V1alpha.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest", + extension_number: 10, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1alpha.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest", + extension_number: 11, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1alpha.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest", + extension_number: 12, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1alpha.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest", + extension_number: 13, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1alpha.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest", + extension_number: 14, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1alpha.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest", + extension_number: 15, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1alpha.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest", + extension_number: 16, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1alpha.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest", + extension_number: 17, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1alpha.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest", + extension_number: 18, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1alpha.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest", + extension_number: 19, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1alpha.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest", + extension_number: 20, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1alpha.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest", + extension_number: 21, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1alpha.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest.GEntry", + extension_number: 10, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1alpha.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest.GEntry", + extension_number: 11, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1alpha.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest.GEntry", + extension_number: 12, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1alpha.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest.GEntry", + extension_number: 13, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1alpha.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest.GEntry", + extension_number: 14, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1alpha.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest.GEntry", + extension_number: 15, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1alpha.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest.GEntry", + extension_number: 16, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1alpha.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest.GEntry", + extension_number: 17, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1alpha.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest.GEntry", + extension_number: 18, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1alpha.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest.GEntry", + extension_number: 19, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1alpha.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest.GEntry", + extension_number: 20, + __unknown_fields__: [] + }}, + {:file_containing_extension, + %Grpc.Reflection.V1alpha.ExtensionRequest{ + containing_type: "testserviceV2.TestRequest.GEntry", + extension_number: 21, + __unknown_fields__: [] + }}, + {:file_containing_symbol, ".grpc.reflection.v1.ServerReflectionRequest"}, + {:file_containing_symbol, ".grpc.reflection.v1.ServerReflectionResponse"}, + {:file_containing_symbol, ".grpc.reflection.v1alpha.ServerReflectionRequest"}, + {:file_containing_symbol, ".grpc.reflection.v1alpha.ServerReflectionResponse"}, + {:file_containing_symbol, ".helloworld.HelloReply"}, + {:file_containing_symbol, ".helloworld.HelloRequest"}, + {:file_containing_symbol, ".testserviceV2.TestReply"}, + {:file_containing_symbol, ".testserviceV2.TestRequest"}, + {:file_containing_symbol, ".testserviceV3.TestReply"}, + {:file_containing_symbol, ".testserviceV3.TestRequest"}, + {:file_containing_symbol, "grpc.reflection.v1.ServerReflection"}, + {:file_containing_symbol, "grpc.reflection.v1alpha.ServerReflection"}, + {:file_containing_symbol, "helloworld.Greeter"}, + {:file_containing_symbol, "testserviceV2.TestService"}, + {:file_containing_symbol, "testserviceV3.TestService"}, + {:list_services, ""} + ] + end + + defp links_from_descriptor(%Google.Protobuf.FileDescriptorProto{} = proto) do + file_dependencies(proto) ++ + service_dependencies(proto) ++ + extension_dependencies(proto) + end + + defp file_dependencies(%Google.Protobuf.FileDescriptorProto{dependency: dependencies}) do + Enum.map(dependencies, fn dep_file -> + {:file_by_filename, dep_file} + end) + end + + defp service_dependencies(%Google.Protobuf.FileDescriptorProto{service: services}) do + Enum.flat_map(services, fn %{method: methods} -> + Enum.flat_map(methods, fn + %{input_type: input, output_type: output} -> + [{:file_containing_symbol, input}, {:file_containing_symbol, output}] + end) + end) + end + + defp extension_dependencies(%Google.Protobuf.FileDescriptorProto{ + package: package, + message_type: types + }) do + gen_range = fn + extendee, ranges -> + ranges + |> Enum.flat_map(fn %{start: start, end: finish} -> start..finish//1 end) + |> Enum.map(fn num -> + {:file_containing_extension, + %Grpc.Reflection.V1alpha.ExtensionRequest{ + containing_type: extendee, + extension_number: num + }} + end) + end + + Enum.flat_map(types, fn type -> + extendee = package <> "." <> type.name + extendee_commands = gen_range.(extendee, type.extension_range) + + nested_commands = + Enum.flat_map(type.nested_type, fn nested_type -> + extendee = extendee <> "." <> nested_type.name + gen_range.(extendee, type.extension_range) + end) + + extendee_commands ++ nested_commands + end) + end end diff --git a/test/service/builder_test.exs b/test/service/builder_test.exs index 40e8129..b73c88a 100644 --- a/test/service/builder_test.exs +++ b/test/service/builder_test.exs @@ -140,4 +140,21 @@ defmodule GrpcReflection.Service.BuilderTest do assert {:ok, tree} = Builder.build_reflection_tree([WrappedService]) assert %State{services: [WrappedService]} = tree end + + @tag skip: "Recursive message structure currently fails to parse" + test "handles a recursive message structure" do + assert {:ok, tree} = Builder.build_reflection_tree([RecursiveMessage.Service.Service]) + + assert tree.files |> Map.keys() |> Enum.sort() == [ + "recursive_message.Reply.proto", + "recursive_message.Service.proto" + ] + + assert tree.symbols |> Map.keys() |> Enum.sort() == [ + "recursive_message.Reply", + "recursive_message.Request", + "recursive_message.Service", + "recursive_message.Service.call" + ] + end end diff --git a/test/support/protos/recursive_message.pb.ex b/test/support/protos/recursive_message.pb.ex new file mode 100644 index 0000000..c71f370 --- /dev/null +++ b/test/support/protos/recursive_message.pb.ex @@ -0,0 +1,120 @@ +defmodule RecursiveMessage.Request do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + def descriptor do + # credo:disable-for-next-line + %Google.Protobuf.DescriptorProto{ + name: "Request", + field: [ + %Google.Protobuf.FieldDescriptorProto{ + name: "reply", + extendee: nil, + number: 1, + label: :LABEL_OPTIONAL, + type: :TYPE_MESSAGE, + type_name: ".recursive_message.Reply", + default_value: nil, + options: nil, + oneof_index: nil, + json_name: "reply", + proto3_optional: nil, + __unknown_fields__: [] + } + ], + nested_type: [], + enum_type: [], + extension_range: [], + extension: [], + options: nil, + oneof_decl: [], + reserved_range: [], + reserved_name: [], + __unknown_fields__: [] + } + end + + field :reply, 1, type: RecursiveMessage.Reply +end + +defmodule RecursiveMessage.Reply do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + def descriptor do + # credo:disable-for-next-line + %Google.Protobuf.DescriptorProto{ + name: "Reply", + field: [ + %Google.Protobuf.FieldDescriptorProto{ + name: "request", + extendee: nil, + number: 1, + label: :LABEL_OPTIONAL, + type: :TYPE_MESSAGE, + type_name: ".recursive_message.Request", + default_value: nil, + options: nil, + oneof_index: nil, + json_name: "request", + proto3_optional: nil, + __unknown_fields__: [] + } + ], + nested_type: [], + enum_type: [], + extension_range: [], + extension: [], + options: nil, + oneof_decl: [], + reserved_range: [], + reserved_name: [], + __unknown_fields__: [] + } + end + + field :request, 1, type: RecursiveMessage.Request +end + +defmodule RecursiveMessage.Service.Service do + @moduledoc false + + use GRPC.Service, name: "recursive_message.Service", protoc_gen_elixir_version: "0.14.1" + + def descriptor do + # credo:disable-for-next-line + %Google.Protobuf.ServiceDescriptorProto{ + name: "Service", + method: [ + %Google.Protobuf.MethodDescriptorProto{ + name: "call", + input_type: ".recursive_message.Request", + output_type: ".recursive_message.Reply", + options: %Google.Protobuf.MethodOptions{ + deprecated: false, + idempotency_level: :IDEMPOTENCY_UNKNOWN, + features: nil, + uninterpreted_option: [], + __pb_extensions__: %{}, + __unknown_fields__: [] + }, + client_streaming: false, + server_streaming: false, + __unknown_fields__: [] + } + ], + options: nil, + __unknown_fields__: [] + } + end + + rpc :call, RecursiveMessage.Request, RecursiveMessage.Reply +end + +defmodule RecursiveMessage.Service.Stub do + @moduledoc false + + use GRPC.Stub, service: RecursiveMessage.Service.Service +end From d2679528309ce37056fb399c4dc20c802b77d9d2 Mon Sep 17 00:00:00 2001 From: mjheilmann Date: Tue, 30 Dec 2025 23:07:29 -0500 Subject: [PATCH 2/2] combine test graph traversal into a utility --- test/integration/v1_reflection_test.exs | 122 +-------------- test/integration/v1alpha_reflection_test.exs | 122 +-------------- test/support/client.ex | 151 +++++++++++++++++++ 3 files changed, 155 insertions(+), 240 deletions(-) create mode 100644 test/support/client.ex diff --git a/test/integration/v1_reflection_test.exs b/test/integration/v1_reflection_test.exs index fd6e7af..569d623 100644 --- a/test/integration/v1_reflection_test.exs +++ b/test/integration/v1_reflection_test.exs @@ -2,24 +2,10 @@ defmodule GrpcReflection.V1ReflectionTest do @moduledoc false use GrpcCase + use GrpcReflection.TestClient, version: :v1 @moduletag capture_log: true - @endpoint GrpcReflection.TestEndpoint.Endpoint - setup_all do - Protobuf.load_extensions() - - {:ok, _pid, port} = GRPC.Server.start_endpoint(@endpoint, 0) - on_exit(fn -> :ok = GRPC.Server.stop_endpoint(@endpoint, []) end) - start_supervised({GRPC.Client.Supervisor, []}) - - host = "localhost:#{port}" - {:ok, channel} = GRPC.Stub.connect(host) - req = %Grpc.Reflection.V1.ServerReflectionRequest{host: host} - - %{channel: channel, req: req, stub: GrpcReflection.TestEndpoint.V1Server.Stub} - end - test "unsupported call is rejected", ctx do message = {:file_containing_extension, %Grpc.Reflection.V1.ExtensionRequest{}} assert {:error, _} = run_request(message, ctx) @@ -258,59 +244,7 @@ defmodule GrpcReflection.V1ReflectionTest do end test "reflection graph is traversable", ctx do - ops = - Stream.unfold([{:list_services, ""}], fn - [] -> - nil - - [{:list_services, ""} = message | rest] = term -> - # add commands to get the files for the listed services - {:ok, %{service: service_list}} = run_request(message, ctx) - - commands = - Enum.map(service_list, fn %{name: service_name} -> - {:file_containing_symbol, service_name} - end) - - {term, commands ++ rest} - - [{:file_by_filename, _} = message | rest] = term -> - message - |> run_request(ctx) - |> case do - {:ok, descriptor} -> - commands = links_from_descriptor(descriptor) - {term, commands ++ rest} - end - - [{:file_containing_extension, _} = message | rest] = term -> - message - |> run_request(ctx) - |> case do - {:ok, _descriptor} -> - # extensions depend on the base type - # if we load the dependencies and feed it into the unfold action - # we get stuck in a loop - {term, rest} - - {:error, %{error_message: "extension not found"}} -> - {term, rest} - end - - [{:file_containing_symbol, _} = message | rest] = term -> - # get the file containing the symbol, and add commands to get the dependencies - message - |> run_request(ctx) - |> case do - {:ok, descriptor} -> - commands = links_from_descriptor(descriptor) - {term, commands ++ rest} - end - end) - |> Enum.to_list() - |> List.flatten() - |> Enum.uniq() - |> Enum.sort() + ops = GrpcReflection.TestClient.traverse_service(ctx) assert ops == [ {:file_by_filename, "google.protobuf.Any.proto"}, @@ -502,56 +436,4 @@ defmodule GrpcReflection.V1ReflectionTest do {:list_services, ""} ] end - - defp links_from_descriptor(%Google.Protobuf.FileDescriptorProto{} = proto) do - file_dependencies(proto) ++ - service_dependencies(proto) ++ - extension_dependencies(proto) - end - - defp file_dependencies(%Google.Protobuf.FileDescriptorProto{dependency: dependencies}) do - Enum.map(dependencies, fn dep_file -> - {:file_by_filename, dep_file} - end) - end - - defp service_dependencies(%Google.Protobuf.FileDescriptorProto{service: services}) do - Enum.flat_map(services, fn %{method: methods} -> - Enum.flat_map(methods, fn - %{input_type: input, output_type: output} -> - [{:file_containing_symbol, input}, {:file_containing_symbol, output}] - end) - end) - end - - defp extension_dependencies(%Google.Protobuf.FileDescriptorProto{ - package: package, - message_type: types - }) do - gen_range = fn - extendee, ranges -> - ranges - |> Enum.flat_map(fn %{start: start, end: finish} -> start..finish//1 end) - |> Enum.map(fn num -> - {:file_containing_extension, - %Grpc.Reflection.V1.ExtensionRequest{ - containing_type: extendee, - extension_number: num - }} - end) - end - - Enum.flat_map(types, fn type -> - extendee = package <> "." <> type.name - extendee_commands = gen_range.(extendee, type.extension_range) - - nested_commands = - Enum.flat_map(type.nested_type, fn nested_type -> - extendee = extendee <> "." <> nested_type.name - gen_range.(extendee, type.extension_range) - end) - - extendee_commands ++ nested_commands - end) - end end diff --git a/test/integration/v1alpha_reflection_test.exs b/test/integration/v1alpha_reflection_test.exs index c340899..5012edf 100644 --- a/test/integration/v1alpha_reflection_test.exs +++ b/test/integration/v1alpha_reflection_test.exs @@ -2,24 +2,10 @@ defmodule GrpcReflection.V1alphaReflectionTest do @moduledoc false use GrpcCase + use GrpcReflection.TestClient, version: :v1alpha @moduletag capture_log: true - @endpoint GrpcReflection.TestEndpoint.Endpoint - setup_all do - Protobuf.load_extensions() - - {:ok, _pid, port} = GRPC.Server.start_endpoint(@endpoint, 0) - on_exit(fn -> :ok = GRPC.Server.stop_endpoint(@endpoint, []) end) - start_supervised({GRPC.Client.Supervisor, []}) - - host = "localhost:#{port}" - {:ok, channel} = GRPC.Stub.connect(host) - req = %Grpc.Reflection.V1alpha.ServerReflectionRequest{host: host} - - %{channel: channel, req: req, stub: GrpcReflection.TestEndpoint.V1AlphaServer.Stub} - end - test "unsupported call is rejected", ctx do message = {:file_containing_extension, %Grpc.Reflection.V1alpha.ExtensionRequest{}} assert {:error, _} = run_request(message, ctx) @@ -261,59 +247,7 @@ defmodule GrpcReflection.V1alphaReflectionTest do end test "reflection graph is traversable", ctx do - ops = - Stream.unfold([{:list_services, ""}], fn - [] -> - nil - - [{:list_services, ""} = message | rest] = term -> - # add commands to get the files for the listed services - {:ok, %{service: service_list}} = run_request(message, ctx) - - commands = - Enum.map(service_list, fn %{name: service_name} -> - {:file_containing_symbol, service_name} - end) - - {term, commands ++ rest} - - [{:file_by_filename, _} = message | rest] = term -> - message - |> run_request(ctx) - |> case do - {:ok, descriptor} -> - commands = links_from_descriptor(descriptor) - {term, commands ++ rest} - end - - [{:file_containing_extension, _} = message | rest] = term -> - message - |> run_request(ctx) - |> case do - {:ok, _descriptor} -> - # extensions depend on the base type - # if we load the dependencies and feed it into the unfold action - # we get stuck in a loop - {term, rest} - - {:error, %{error_message: "extension not found"}} -> - {term, rest} - end - - [{:file_containing_symbol, _} = message | rest] = term -> - # get the file containing the symbol, and add commands to get the dependencies - message - |> run_request(ctx) - |> case do - {:ok, descriptor} -> - commands = links_from_descriptor(descriptor) - {term, commands ++ rest} - end - end) - |> Enum.to_list() - |> List.flatten() - |> Enum.uniq() - |> Enum.sort() + ops = GrpcReflection.TestClient.traverse_service(ctx) assert ops == [ {:file_by_filename, "google.protobuf.Any.proto"}, @@ -505,56 +439,4 @@ defmodule GrpcReflection.V1alphaReflectionTest do {:list_services, ""} ] end - - defp links_from_descriptor(%Google.Protobuf.FileDescriptorProto{} = proto) do - file_dependencies(proto) ++ - service_dependencies(proto) ++ - extension_dependencies(proto) - end - - defp file_dependencies(%Google.Protobuf.FileDescriptorProto{dependency: dependencies}) do - Enum.map(dependencies, fn dep_file -> - {:file_by_filename, dep_file} - end) - end - - defp service_dependencies(%Google.Protobuf.FileDescriptorProto{service: services}) do - Enum.flat_map(services, fn %{method: methods} -> - Enum.flat_map(methods, fn - %{input_type: input, output_type: output} -> - [{:file_containing_symbol, input}, {:file_containing_symbol, output}] - end) - end) - end - - defp extension_dependencies(%Google.Protobuf.FileDescriptorProto{ - package: package, - message_type: types - }) do - gen_range = fn - extendee, ranges -> - ranges - |> Enum.flat_map(fn %{start: start, end: finish} -> start..finish//1 end) - |> Enum.map(fn num -> - {:file_containing_extension, - %Grpc.Reflection.V1alpha.ExtensionRequest{ - containing_type: extendee, - extension_number: num - }} - end) - end - - Enum.flat_map(types, fn type -> - extendee = package <> "." <> type.name - extendee_commands = gen_range.(extendee, type.extension_range) - - nested_commands = - Enum.flat_map(type.nested_type, fn nested_type -> - extendee = extendee <> "." <> nested_type.name - gen_range.(extendee, type.extension_range) - end) - - extendee_commands ++ nested_commands - end) - end end diff --git a/test/support/client.ex b/test/support/client.ex new file mode 100644 index 0000000..2fe6e76 --- /dev/null +++ b/test/support/client.ex @@ -0,0 +1,151 @@ +defmodule GrpcReflection.TestClient do + use ExUnit.CaseTemplate + import GrpcCase + + defmacro __using__(version: version) do + endpoint = GrpcReflection.TestEndpoint.Endpoint + + quote do + setup_all do + Protobuf.load_extensions() + + {:ok, _pid, port} = GRPC.Server.start_endpoint(unquote(endpoint), 0) + on_exit(fn -> :ok = GRPC.Server.stop_endpoint(unquote(endpoint), []) end) + start_supervised({GRPC.Client.Supervisor, []}) + + host = "localhost:#{port}" + {:ok, channel} = GRPC.Stub.connect(host) + + req = + case unquote(version) do + :v1 -> %Grpc.Reflection.V1.ServerReflectionRequest{host: host} + :v1alpha -> %Grpc.Reflection.V1alpha.ServerReflectionRequest{host: host} + end + + stub = + case unquote(version) do + :v1 -> GrpcReflection.TestEndpoint.V1Server.Stub + :v1alpha -> GrpcReflection.TestEndpoint.V1AlphaServer.Stub + end + + %{channel: channel, req: req, stub: stub, version: unquote(version)} + end + end + end + + def traverse_service(ctx) do + Stream.unfold([{:list_services, ""}], fn + [] -> + nil + + [{:list_services, ""} = message | rest] = term -> + # add commands to get the files for the listed services + {:ok, %{service: service_list}} = run_request(message, ctx) + + commands = + Enum.map(service_list, fn %{name: service_name} -> + {:file_containing_symbol, service_name} + end) + + {term, commands ++ rest} + + [{:file_by_filename, _} = message | rest] = term -> + message + |> run_request(ctx) + |> case do + {:ok, descriptor} -> + commands = links_from_descriptor(descriptor, ctx) + {term, commands ++ rest} + end + + [{:file_containing_extension, _} = message | rest] = term -> + message + |> run_request(ctx) + |> case do + {:ok, _descriptor} -> + # extensions depend on the base type + # if we load the dependencies and feed it into the unfold action + # we get stuck in a loop + {term, rest} + + {:error, %{error_message: "extension not found"}} -> + {term, rest} + end + + [{:file_containing_symbol, _} = message | rest] = term -> + # get the file containing the symbol, and add commands to get the dependencies + message + |> run_request(ctx) + |> case do + {:ok, descriptor} -> + commands = links_from_descriptor(descriptor, ctx) + {term, commands ++ rest} + end + end) + |> Enum.to_list() + |> List.flatten() + |> Enum.uniq() + |> Enum.sort() + end + + defp links_from_descriptor(%Google.Protobuf.FileDescriptorProto{} = proto, ctx) do + file_dependencies(proto) ++ + service_dependencies(proto) ++ + extension_dependencies(proto, ctx) + end + + defp file_dependencies(%Google.Protobuf.FileDescriptorProto{dependency: dependencies}) do + Enum.map(dependencies, fn dep_file -> + {:file_by_filename, dep_file} + end) + end + + defp service_dependencies(%Google.Protobuf.FileDescriptorProto{service: services}) do + Enum.flat_map(services, fn %{method: methods} -> + Enum.flat_map(methods, fn + %{input_type: input, output_type: output} -> + [{:file_containing_symbol, input}, {:file_containing_symbol, output}] + end) + end) + end + + defp extension_dependencies( + %Google.Protobuf.FileDescriptorProto{ + package: package, + message_type: types + }, + %{version: version} + ) do + request_mod = + case version do + :v1 -> Grpc.Reflection.V1.ExtensionRequest + :v1alpha -> Grpc.Reflection.V1alpha.ExtensionRequest + end + + gen_range = fn + extendee, ranges -> + ranges + |> Enum.flat_map(fn %{start: start, end: finish} -> start..finish//1 end) + |> Enum.map(fn num -> + {:file_containing_extension, + struct(request_mod, %{ + containing_type: extendee, + extension_number: num + })} + end) + end + + Enum.flat_map(types, fn type -> + extendee = package <> "." <> type.name + extendee_commands = gen_range.(extendee, type.extension_range) + + nested_commands = + Enum.flat_map(type.nested_type, fn nested_type -> + extendee = extendee <> "." <> nested_type.name + gen_range.(extendee, type.extension_range) + end) + + extendee_commands ++ nested_commands + end) + end +end