This is an automated email from the ASF dual-hosted git repository.

chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git


The following commit(s) were added to refs/heads/main by this push:
     new 18b821c72 test(compiler): add IR validation and codegen tests for gRPC 
service support (#3528)
18b821c72 is described below

commit 18b821c7290843dfcdc0f6b84835c6102715a812
Author: Darius <[email protected]>
AuthorDate: Mon Mar 30 15:13:18 2026 +0300

    test(compiler): add IR validation and codegen tests for gRPC service 
support (#3528)
    
    ## Why?
    
    Issue #3277 requires unit tests for gRPC service support in the Fory
    compiler: IR validation tests covering name, type, and streaming rules,
    and golden codegen tests for service schemas. The parser tests for
    FDL/proto/fbs service syntax already existed from closed sub-issues.
    This PR fills the remaining gaps.
    
    ## What does this PR do?
    
    **`compiler/fory_compiler/ir/validator.py`**
    - Added `_check_services()` implementing three validation rules:
      1. Duplicate service names → error
      2. Duplicate method names within the same service → error
    3. RPC request/response types must be `message` — `enum` and `union` are
    rejected
    - Wired `_check_services()` into `validate()` after
    `_check_type_references()`
    
    **`compiler/fory_compiler/tests/test_ir_service_validation.py`** (new)
    - Tests for all three validation rules above, across FDL, proto, and fbs
    frontends
    - Tests that streaming RPCs (client, server, bidi) pass validation
    without error
    
    **`compiler/fory_compiler/tests/test_service_codegen.py`** (new)
    - Asserts service definitions do not alter message codegen output across
    all 7 language generators
    - Documents that `generate_services()` returns `[]` as a baseline for
    all generators
    - Verifies `compile_file(..., grpc=True)` succeeds and produces output
    for all languages using `examples/service.fdl`
    - Asserts key signatures (`class HelloRequest`, `String name`, etc.) are
    present in Java and Python generated output
    
    ## Related issues
    
    - Closes #3277
    - Related to #3266
---
 compiler/fory_compiler/ir/validator.py             |  32 +++
 .../tests/test_ir_service_validation.py            | 307 +++++++++++++++++++++
 .../fory_compiler/tests/test_service_codegen.py    | 147 ++++++++++
 3 files changed, 486 insertions(+)

diff --git a/compiler/fory_compiler/ir/validator.py 
b/compiler/fory_compiler/ir/validator.py
index 294657b2f..acd774177 100644
--- a/compiler/fory_compiler/ir/validator.py
+++ b/compiler/fory_compiler/ir/validator.py
@@ -66,6 +66,7 @@ class SchemaValidator:
         self._check_duplicate_type_ids()
         self._check_messages()
         self._check_type_references()
+        self._check_services()
         self._check_collection_nesting()
         self._check_ref_rules()
         self._check_weak_refs()
@@ -639,6 +640,37 @@ class SchemaValidator:
             for f in union.fields:
                 check_field(f, None)
 
+    def _check_services(self) -> None:
+        seen_service_names: dict = {}
+        for service in self.schema.services:
+            if service.name in seen_service_names:
+                self._error(
+                    f"Duplicate service name: {service.name}",
+                    service.location,
+                )
+            seen_service_names.setdefault(service.name, service.location)
+
+            seen_method_names: dict = {}
+            for method in service.methods:
+                if method.name in seen_method_names:
+                    self._error(
+                        f"Duplicate method name in service {service.name}: 
{method.name}",
+                        method.location,
+                    )
+                seen_method_names.setdefault(method.name, method.location)
+
+                for named_type in (method.request_type, method.response_type):
+                    resolved = self.schema.get_type(named_type.name)
+                    if resolved is None:
+                        continue
+                    if not isinstance(resolved, Message):
+                        kind = "enum" if isinstance(resolved, Enum) else 
"union"
+                        self._error(
+                            f"RPC type '{named_type.name}' in service 
{service.name}"
+                            f" must be a message, not a {kind}",
+                            named_type.location,
+                        )
+
 
 def validate_schema(schema: Schema) -> List[str]:
     """Validate a schema and return a list of error messages."""
diff --git a/compiler/fory_compiler/tests/test_ir_service_validation.py 
b/compiler/fory_compiler/tests/test_ir_service_validation.py
new file mode 100644
index 000000000..7abf44f90
--- /dev/null
+++ b/compiler/fory_compiler/tests/test_ir_service_validation.py
@@ -0,0 +1,307 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Tests for IR-level service validation rules."""
+
+from fory_compiler.frontend.fdl.parser import Parser
+from fory_compiler.frontend.proto.lexer import Lexer as ProtoLexer
+from fory_compiler.frontend.proto.parser import Parser as ProtoParser
+from fory_compiler.frontend.proto.translator import ProtoTranslator
+from fory_compiler.frontend.fbs.lexer import Lexer as FbsLexer
+from fory_compiler.frontend.fbs.parser import Parser as FbsParser
+from fory_compiler.frontend.fbs.translator import FbsTranslator
+from fory_compiler.ir.validator import SchemaValidator
+
+
+def parse_fdl(source: str):
+    return Parser.from_source(source).parse()
+
+
+def parse_proto(source: str):
+    lexer = ProtoLexer(source)
+    parser = ProtoParser(lexer.tokenize())
+    return ProtoTranslator(parser.parse()).translate()
+
+
+def parse_fbs(source: str):
+    lexer = FbsLexer(source)
+    parser = FbsParser(lexer.tokenize())
+    return FbsTranslator(parser.parse()).translate()
+
+
+def validate(schema):
+    validator = SchemaValidator(schema)
+    validator.validate()
+    return validator
+
+
+def test_duplicate_service_names_fails_validation():
+    source = """
+    package test;
+
+    service Alpha {}
+    service Alpha {}
+    """
+    v = validate(parse_fdl(source))
+    assert any("Duplicate service name: Alpha" in e.message for e in v.errors)
+
+
+def test_unique_service_names_passes_validation():
+    source = """
+    package test;
+
+    service Alpha {}
+    service Beta {}
+    """
+    v = validate(parse_fdl(source))
+    assert v.errors == []
+
+
+def test_duplicate_method_names_fails_validation():
+    source = """
+    package test;
+
+    message Req {}
+    message Res {}
+
+    service Greeter {
+        rpc SayHello (Req) returns (Res);
+        rpc SayHello (Req) returns (Res);
+    }
+    """
+    v = validate(parse_fdl(source))
+    assert any(
+        "Duplicate method name in service Greeter: SayHello" in e.message
+        for e in v.errors
+    )
+
+
+def test_same_method_name_in_different_services_passes_validation():
+    source = """
+    package test;
+
+    message Req {}
+    message Res {}
+
+    service Alpha {
+        rpc SayHello (Req) returns (Res);
+    }
+
+    service Beta {
+        rpc SayHello (Req) returns (Res);
+    }
+    """
+    v = validate(parse_fdl(source))
+    assert v.errors == []
+
+
+def test_unique_method_names_passes_validation():
+    source = """
+    package test;
+
+    message Req {}
+    message Res {}
+
+    service Greeter {
+        rpc SayHello (Req) returns (Res);
+        rpc SayGoodbye (Req) returns (Res);
+    }
+    """
+    v = validate(parse_fdl(source))
+    assert v.errors == []
+
+
+def test_rpc_request_type_enum_fails_validation():
+    source = """
+    package test;
+
+    enum Status { OK = 0; }
+    message Res {}
+
+    service Svc {
+        rpc Call (Status) returns (Res);
+    }
+    """
+    v = validate(parse_fdl(source))
+    assert any(
+        "RPC type 'Status'" in e.message and "not a enum" in e.message for e 
in v.errors
+    )
+
+
+def test_rpc_response_type_enum_fails_validation():
+    source = """
+    package test;
+
+    message Req {}
+    enum Status { OK = 0; }
+
+    service Svc {
+        rpc Call (Req) returns (Status);
+    }
+    """
+    v = validate(parse_fdl(source))
+    assert any(
+        "RPC type 'Status'" in e.message and "not a enum" in e.message for e 
in v.errors
+    )
+
+
+def test_rpc_request_type_union_fails_validation():
+    source = """
+    package test;
+
+    union Payload { string text = 1; }
+    message Res {}
+
+    service Svc {
+        rpc Call (Payload) returns (Res);
+    }
+    """
+    v = validate(parse_fdl(source))
+    assert any(
+        "RPC type 'Payload'" in e.message and "not a union" in e.message
+        for e in v.errors
+    )
+
+
+def test_rpc_response_type_union_fails_validation():
+    source = """
+    package test;
+
+    message Req {}
+    union Payload { string text = 1; }
+
+    service Svc {
+        rpc Call (Req) returns (Payload);
+    }
+    """
+    v = validate(parse_fdl(source))
+    assert any(
+        "RPC type 'Payload'" in e.message and "not a union" in e.message
+        for e in v.errors
+    )
+
+
+def test_rpc_message_types_pass_validation():
+    source = """
+    package test;
+
+    message Req {}
+    message Res {}
+
+    service Svc {
+        rpc Call (Req) returns (Res);
+    }
+    """
+    v = validate(parse_fdl(source))
+    assert v.errors == []
+
+
+def test_proto_rpc_enum_type_fails_validation():
+    source = """
+    syntax = "proto3";
+    package test;
+
+    enum Status { OK = 0; }
+    message Res { string result = 1; }
+
+    service Svc {
+        rpc Call (Status) returns (Res);
+    }
+    """
+    v = validate(parse_proto(source))
+    assert any("RPC type 'Status'" in e.message for e in v.errors)
+
+
+def test_fbs_rpc_duplicate_method_fails_validation():
+    source = """
+    namespace test;
+
+    table Req { id: int; }
+    table Res { result: string; }
+
+    rpc_service Svc {
+        Call(Req):Res;
+        Call(Req):Res;
+    }
+    """
+    v = validate(parse_fbs(source))
+    assert any(
+        "Duplicate method name in service Svc: Call" in e.message for e in 
v.errors
+    )
+
+
+def test_client_streaming_rpc_passes_validation():
+    source = """
+    package test;
+
+    message Req {}
+    message Res {}
+
+    service Svc {
+        rpc Call (stream Req) returns (Res);
+    }
+    """
+    v = validate(parse_fdl(source))
+    assert v.errors == []
+
+
+def test_server_streaming_rpc_passes_validation():
+    source = """
+    package test;
+
+    message Req {}
+    message Res {}
+
+    service Svc {
+        rpc Call (Req) returns (stream Res);
+    }
+    """
+    v = validate(parse_fdl(source))
+    assert v.errors == []
+
+
+def test_bidi_streaming_rpc_passes_validation():
+    source = """
+    package test;
+
+    message Req {}
+    message Res {}
+
+    service Svc {
+        rpc Call (stream Req) returns (stream Res);
+    }
+    """
+    v = validate(parse_fdl(source))
+    assert v.errors == []
+
+
+def test_proto_streaming_rpc_passes_validation():
+    source = """
+    syntax = "proto3";
+    package test;
+
+    message Req { string id = 1; }
+    message Res { string result = 1; }
+
+    service Svc {
+        rpc ClientStream (stream Req) returns (Res);
+        rpc ServerStream (Req) returns (stream Res);
+        rpc BidiStream (stream Req) returns (stream Res);
+    }
+    """
+    v = validate(parse_proto(source))
+    assert v.errors == []
diff --git a/compiler/fory_compiler/tests/test_service_codegen.py 
b/compiler/fory_compiler/tests/test_service_codegen.py
new file mode 100644
index 000000000..e27425d1f
--- /dev/null
+++ b/compiler/fory_compiler/tests/test_service_codegen.py
@@ -0,0 +1,147 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Codegen smoke tests for schemas that contain service definitions."""
+
+from pathlib import Path
+from textwrap import dedent
+from typing import Dict, Tuple, Type
+
+from fory_compiler.cli import compile_file
+from fory_compiler.frontend.fdl.lexer import Lexer
+from fory_compiler.frontend.fdl.parser import Parser
+from fory_compiler.generators.base import BaseGenerator, GeneratorOptions
+from fory_compiler.generators.cpp import CppGenerator
+from fory_compiler.generators.csharp import CSharpGenerator
+from fory_compiler.generators.go import GoGenerator
+from fory_compiler.generators.java import JavaGenerator
+from fory_compiler.generators.python import PythonGenerator
+from fory_compiler.generators.rust import RustGenerator
+from fory_compiler.generators.swift import SwiftGenerator
+from fory_compiler.ir.ast import Schema
+
+
+GENERATOR_CLASSES: Tuple[Type[BaseGenerator], ...] = (
+    JavaGenerator,
+    PythonGenerator,
+    CppGenerator,
+    RustGenerator,
+    GoGenerator,
+    CSharpGenerator,
+    SwiftGenerator,
+)
+
+_GREETER_WITH_SERVICE = dedent(
+    """
+    package demo.greeter;
+
+    message HelloRequest {
+        string name = 1;
+    }
+
+    message HelloReply {
+        string reply = 1;
+    }
+
+    service Greeter {
+        rpc SayHello (HelloRequest) returns (HelloReply);
+    }
+    """
+)
+
+_GREETER_WITHOUT_SERVICE = dedent(
+    """
+    package demo.greeter;
+
+    message HelloRequest {
+        string name = 1;
+    }
+
+    message HelloReply {
+        string reply = 1;
+    }
+    """
+)
+
+
+def parse_fdl(source: str) -> Schema:
+    return Parser(Lexer(source).tokenize()).parse()
+
+
+def generate_files(
+    schema: Schema, generator_cls: Type[BaseGenerator]
+) -> Dict[str, str]:
+    options = GeneratorOptions(output_dir=Path("/tmp"))
+    generator = generator_cls(schema, options)
+    return {item.path: item.content for item in generator.generate()}
+
+
+def test_service_definition_does_not_affect_message_codegen():
+    schema_with = parse_fdl(_GREETER_WITH_SERVICE)
+    schema_without = parse_fdl(_GREETER_WITHOUT_SERVICE)
+    for generator_cls in GENERATOR_CLASSES:
+        files_with = generate_files(schema_with, generator_cls)
+        files_without = generate_files(schema_without, generator_cls)
+        assert files_with == files_without, (
+            f"{generator_cls.language_name}: service definition changed 
message output"
+        )
+
+
+def test_generate_services_returns_empty_list_for_all_generators():
+    schema = parse_fdl(_GREETER_WITH_SERVICE)
+    for generator_cls in GENERATOR_CLASSES:
+        options = GeneratorOptions(output_dir=Path("/tmp"))
+        generator = generator_cls(schema, options)
+        assert generator.generate_services() == [], (
+            f"{generator_cls.language_name}: generate_services() should return 
[] until gRPC is implemented"
+        )
+
+
+def test_service_schema_produces_one_file_per_message_per_language():
+    schema = parse_fdl(_GREETER_WITH_SERVICE)
+    for generator_cls in GENERATOR_CLASSES:
+        files = generate_files(schema, generator_cls)
+        assert len(files) >= 1, (
+            f"{generator_cls.language_name}: expected at least one generated 
file"
+        )
+
+
+def test_compile_service_schema_with_grpc_flag(tmp_path: Path):
+    example_path = Path(__file__).resolve().parents[2] / "examples" / 
"service.fdl"
+    lang_dirs = {}
+    for lang in ("java", "python", "rust", "go", "cpp", "csharp", "swift"):
+        lang_dirs[lang] = tmp_path / lang
+    ok = compile_file(example_path, lang_dirs, grpc=True)
+    assert ok is True
+    for lang, lang_dir in lang_dirs.items():
+        files = [p for p in lang_dir.rglob("*") if p.is_file()]
+        assert len(files) >= 1, f"{lang}: expected at least one file with 
grpc=True"
+
+
+def test_generated_message_contains_key_signatures():
+    schema = parse_fdl(_GREETER_WITH_SERVICE)
+    java_files = generate_files(schema, JavaGenerator)
+    all_java = "\n".join(java_files.values())
+    assert "class HelloRequest" in all_java
+    assert "class HelloReply" in all_java
+    assert "String name" in all_java
+    assert "String reply" in all_java
+
+    python_files = generate_files(schema, PythonGenerator)
+    all_python = "\n".join(python_files.values())
+    assert "HelloRequest" in all_python
+    assert "HelloReply" in all_python


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to