From 3c3d5f2d0228e3394ab4fb78ba5f90a6a0bd0114 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Sun, 2 Aug 2026 05:03:27 -0400 Subject: [PATCH 1/2] fix(protoc-gen-elixir): emit one file per service under one_file_per_module The escript groups a service's Service and Stub defmodules into a single foo_service.pb.ex; matching it removes the last known output divergence so the escript parity suite can assert byte identity unconditionally. Signed-off-by: Yordis Prieto --- cmd/protoc-gen-elixir/generator.go | 23 ++++++------- .../one_file_per_module_test.go | 32 ++++++++----------- 2 files changed, 25 insertions(+), 30 deletions(-) diff --git a/cmd/protoc-gen-elixir/generator.go b/cmd/protoc-gen-elixir/generator.go index 642ca76..c0ca57c 100644 --- a/cmd/protoc-gen-elixir/generator.go +++ b/cmd/protoc-gen-elixir/generator.go @@ -173,9 +173,10 @@ func renderFileExtensionGroups(files []*descriptorpb.FileDescriptorProto, params // both a service AND a message-embedded extend in the same file, so the // exact service-vs-this-tier relative order is inferred from that // original table, not independently proven. -// 5. Services (gated on plugins=grpc), in file declaration order, each as a -// paired ".Service" + ".Stub" module - rendered after every message body, -// verified against testdata/golden/grpc/test/service.pb.ex and +// 5. Services (gated on plugins=grpc), in file declaration order, each as +// one module pairing its ".Service" and ".Stub" defmodules - rendered +// after every message body, verified against +// testdata/golden/grpc/test/service.pb.ex and // testdata/golden/grpc_proto_source/test/service.pb.ex. // // The file-level merged PbExtension module is NOT rendered here at all - see @@ -284,13 +285,14 @@ func (ctx *fileRenderContext) renderEnum(enum *descriptorpb.EnumDescriptorProto, // renderService renders a single service's paired .Service/.Stub modules // given the file's base module name / base package and the service's own -// SourceCodeInfo path (e.g. [6, i] for file-level service i). The two -// modules are returned as separate renderedModule entries (each with its own -// module name) since one_file_per_module=true emits them as two distinct -// files. +// SourceCodeInfo path (e.g. [6, i] for file-level service i). The pair is +// returned as ONE renderedModule keyed by the bare service module name (no +// .Service/.Stub suffix), matching the escript, which emits both defmodules +// into a single file derived from that bare name. func (ctx *fileRenderContext) renderService(svc *descriptorpb.ServiceDescriptorProto, path []int32) ([]renderedModule, error) { - serviceModName := qualifyModName(ctx.baseMod, CamelizeEach(svc.GetName())) + ".Service" - stubModName := qualifyModName(ctx.baseMod, CamelizeEach(svc.GetName())) + ".Stub" + modName := qualifyModName(ctx.baseMod, CamelizeEach(svc.GetName())) + serviceModName := modName + ".Service" + stubModName := modName + ".Stub" fullName := qualifyFullName(ctx.basePkg, svc.GetName()) var docComment string @@ -309,8 +311,7 @@ func (ctx *fileRenderContext) renderService(svc *descriptorpb.ServiceDescriptorP } return []renderedModule{ - {modName: serviceModName, text: serviceText}, - {modName: stubModName, text: stubText}, + {modName: modName, text: serviceText + "\n\n" + stubText}, }, nil } diff --git a/cmd/protoc-gen-elixir/one_file_per_module_test.go b/cmd/protoc-gen-elixir/one_file_per_module_test.go index 92f9ee2..be2c7ea 100644 --- a/cmd/protoc-gen-elixir/one_file_per_module_test.go +++ b/cmd/protoc-gen-elixir/one_file_per_module_test.go @@ -63,17 +63,15 @@ func TestOneFilePerModuleIntegration(t *testing.T) { assert.Len(t, resp.GetFile(), len(goldenPaths), "generated file count should match golden fixture count exactly") } -// TestOneFilePerModuleWithGRPC guards against a regression where the -// .Service/.Stub pair was split apart via strings.Cut(text, "\n\n") on -// RenderService's combined output: the .Service module body itself contains -// internal blank lines (e.g. after a multi-line @moduledoc, before "use -// GRPC.Service") that appear before the true Service/Stub boundary, so -// cutting on the first "\n\n" silently truncated the .Service file and -// prepended garbage to the .Stub file. There's no golden fixture combining +// TestOneFilePerModuleWithGRPC guards the escript-matching file layout: the +// .Service and .Stub defmodules for a given service are emitted together in +// ONE file, named from the bare service module name (no .Service/.Stub +// suffix) via Macro.underscore. There's no golden fixture combining // one_file_per_module=true with plugins=grpc (gen_goldens.sh never generates // one), so this asserts against testdata/golden/grpc/test/service.pb.ex - -// the proven-correct single-file rendering of the same test.proto/ -// service.proto pair - split at its own known-correct module boundary. +// the proven-correct rendering of the same test.proto/service.proto pair - +// which is exactly the expected content here since that fixture already +// pairs both defmodules in one blob. func TestOneFilePerModuleWithGRPC(t *testing.T) { t.Parallel() @@ -88,7 +86,7 @@ func TestOneFilePerModuleWithGRPC(t *testing.T) { resp := testGenerate(t, req) require.Empty(t, resp.GetError()) - wantService := `defmodule Test.TestService.Service do + want := `defmodule Test.TestService.Service do @moduledoc """ An example test service that has a test method. It expects a Request @@ -99,17 +97,13 @@ func TestOneFilePerModuleWithGRPC(t *testing.T) { rpc :test, Test.Request, Test.Reply end -` - wantStub := `defmodule Test.TestService.Stub do + +defmodule Test.TestService.Stub do use GRPC.Stub, service: Test.TestService.Service end ` - serviceFile, ok := findGeneratedFile(resp.GetFile(), "test/test_service/service.pb.ex") - require.True(t, ok, "expected test/test_service/service.pb.ex among generated files") - assert.Equal(t, wantService, serviceFile.GetContent()) - - stubFile, ok := findGeneratedFile(resp.GetFile(), "test/test_service/stub.pb.ex") - require.True(t, ok, "expected test/test_service/stub.pb.ex among generated files") - assert.Equal(t, wantStub, stubFile.GetContent()) + file, ok := findGeneratedFile(resp.GetFile(), "test/test_service.pb.ex") + require.True(t, ok, "expected test/test_service.pb.ex among generated files") + assert.Equal(t, want, file.GetContent()) } From e62d5bcd4c097fd914fb73bdc828ab72944cd6c3 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Sun, 2 Aug 2026 05:09:04 -0400 Subject: [PATCH 2/2] test(protoc-gen-elixir): assert exact ofpm grpc file set Content-only assertions would still pass if the legacy split .Service/.Stub files were emitted alongside the merged file. Signed-off-by: Yordis Prieto --- cmd/protoc-gen-elixir/one_file_per_module_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/protoc-gen-elixir/one_file_per_module_test.go b/cmd/protoc-gen-elixir/one_file_per_module_test.go index be2c7ea..bef7402 100644 --- a/cmd/protoc-gen-elixir/one_file_per_module_test.go +++ b/cmd/protoc-gen-elixir/one_file_per_module_test.go @@ -103,6 +103,8 @@ defmodule Test.TestService.Stub do end ` + require.Len(t, resp.GetFile(), 1, "service.proto under one_file_per_module should yield exactly the merged service file, with no separate .Service/.Stub files") + file, ok := findGeneratedFile(resp.GetFile(), "test/test_service.pb.ex") require.True(t, ok, "expected test/test_service.pb.ex among generated files") assert.Equal(t, want, file.GetContent())