diff --git a/betterproto/tests/README.md b/betterproto/tests/README.md index de2e2d2..1892cea 100644 --- a/betterproto/tests/README.md +++ b/betterproto/tests/README.md @@ -87,4 +87,4 @@ betterproto/tests/test_inputs.py ..x...x..x...x.X........xx........x.....x...... - `x` — XFAIL: expected failure - `X` — XPASS: expected failure, but still passed -Test cases marked for expected failure are declared in [inputs/xfail.py](inputs.xfail.py) \ No newline at end of file +Test cases marked for expected failure are declared in [inputs/config.py](inputs/config.py) \ No newline at end of file diff --git a/betterproto/tests/generate.py b/betterproto/tests/generate.py index f3b92e6..fc85b7f 100644 --- a/betterproto/tests/generate.py +++ b/betterproto/tests/generate.py @@ -2,6 +2,7 @@ import glob import os import shutil +import subprocess import sys from typing import Set @@ -33,6 +34,8 @@ def generate(whitelist: Set[str]): test_case_names = set(get_directories(inputs_path)) + failed_test_cases = [] + for test_case_name in sorted(test_case_names): test_case_input_path = os.path.realpath( os.path.join(inputs_path, test_case_name) @@ -45,22 +48,39 @@ def generate(whitelist: Set[str]): ): continue - test_case_output_path_reference = os.path.join( - output_path_reference, test_case_name - ) - test_case_output_path_betterproto = os.path.join( - output_path_betterproto, test_case_name - ) - print(f"Generating output for {test_case_name}") - os.makedirs(test_case_output_path_reference, exist_ok=True) - os.makedirs(test_case_output_path_betterproto, exist_ok=True) + try: + generate_test_case_output(test_case_name, test_case_input_path) + except subprocess.CalledProcessError as e: + failed_test_cases.append(test_case_name) - clear_directory(test_case_output_path_reference) - clear_directory(test_case_output_path_betterproto) + if failed_test_cases: + sys.stderr.write("\nFailed to generate the following test cases:\n") + for failed_test_case in failed_test_cases: + sys.stderr.write(f"- {failed_test_case}\n") - protoc_reference(test_case_input_path, test_case_output_path_reference) - protoc_plugin(test_case_input_path, test_case_output_path_betterproto) + +def generate_test_case_output(test_case_name, test_case_input_path=None): + if not test_case_input_path: + test_case_input_path = os.path.realpath( + os.path.join(inputs_path, test_case_name) + ) + + test_case_output_path_reference = os.path.join( + output_path_reference, test_case_name + ) + test_case_output_path_betterproto = os.path.join( + output_path_betterproto, test_case_name + ) + + os.makedirs(test_case_output_path_reference, exist_ok=True) + os.makedirs(test_case_output_path_betterproto, exist_ok=True) + + clear_directory(test_case_output_path_reference) + clear_directory(test_case_output_path_betterproto) + + protoc_reference(test_case_input_path, test_case_output_path_reference) + protoc_plugin(test_case_input_path, test_case_output_path_betterproto) HELP = "\n".join( diff --git a/betterproto/tests/inputs/casing_message_field_uppercase/casing_message_field_uppercase.json b/betterproto/tests/inputs/casing_message_field_uppercase/casing_message_field_uppercase.json new file mode 100644 index 0000000..83bd111 --- /dev/null +++ b/betterproto/tests/inputs/casing_message_field_uppercase/casing_message_field_uppercase.json @@ -0,0 +1,5 @@ +{ + "UPPERCASE": 10, + "UPPERCASE_V2": 10, + "UPPER_CAMEL_CASE": 10 +} diff --git a/betterproto/tests/inputs/casing_message_field_uppercase/casing_message_field_uppercase.proto b/betterproto/tests/inputs/casing_message_field_uppercase/casing_message_field_uppercase.proto new file mode 100644 index 0000000..9964dfa --- /dev/null +++ b/betterproto/tests/inputs/casing_message_field_uppercase/casing_message_field_uppercase.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +message Test { + int32 UPPERCASE = 1; + int32 UPPERCASE_V2 = 2; + int32 UPPER_CAMEL_CASE = 3; +} \ No newline at end of file diff --git a/betterproto/tests/inputs/casing_message_field_uppercase/casing_message_field_uppercase.py b/betterproto/tests/inputs/casing_message_field_uppercase/casing_message_field_uppercase.py new file mode 100644 index 0000000..d77119e --- /dev/null +++ b/betterproto/tests/inputs/casing_message_field_uppercase/casing_message_field_uppercase.py @@ -0,0 +1,16 @@ +from betterproto.tests.output_betterproto.casing_message_field_uppercase.casing_message_field_uppercase import ( + Test, +) + + +def test_message_casing(): + message = Test() + assert hasattr( + message, "uppercase" + ), "UPPERCASE attribute is converted to 'uppercase' in python" + assert hasattr( + message, "uppercase_v2" + ), "UPPERCASE_V2 attribute is converted to 'uppercase_v2' in python" + assert hasattr( + message, "upper_camel_case" + ), "UPPER_CAMEL_CASE attribute is converted to upper_camel_case in python" diff --git a/betterproto/tests/inputs/config.py b/betterproto/tests/inputs/config.py new file mode 100644 index 0000000..cf4c996 --- /dev/null +++ b/betterproto/tests/inputs/config.py @@ -0,0 +1,23 @@ +# Test cases that are expected to fail, e.g. unimplemented features or bug-fixes. +# Remove from list when fixed. +tests = { + "import_root_sibling", # 61 + "import_child_package_from_package", # 58 + "import_root_package_from_child", # 60 + "import_parent_package_from_child", # 59 + "import_circular_dependency", # failing because of other bugs now + "import_packages_same_name", # 25 + "oneof_enum", # 63 + "googletypes_service_returns_empty", # 9 + "casing_message_field_uppercase", # 11 + "namespace_keywords", # 70 + "namespace_builtin_types" # 53 +} + +services = { + "googletypes_response", + "googletypes_response_embedded", + "service", + "import_service_input_message", + "googletypes_service_returns_empty", +} diff --git a/betterproto/tests/inputs/googletypes_service_returns_empty/googletypes_service_returns_empty.proto b/betterproto/tests/inputs/googletypes_service_returns_empty/googletypes_service_returns_empty.proto new file mode 100644 index 0000000..c454691 --- /dev/null +++ b/betterproto/tests/inputs/googletypes_service_returns_empty/googletypes_service_returns_empty.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +import "google/protobuf/empty.proto"; + +service Test { + rpc Send (RequestMessage) returns (google.protobuf.Empty) { + } +} + +message RequestMessage { +} \ No newline at end of file diff --git a/betterproto/tests/inputs/import_packages_same_name/import_packages_same_name.proto b/betterproto/tests/inputs/import_packages_same_name/import_packages_same_name.proto new file mode 100644 index 0000000..c1f28e9 --- /dev/null +++ b/betterproto/tests/inputs/import_packages_same_name/import_packages_same_name.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +import "users_v1.proto"; +import "posts_v1.proto"; + +// Tests generated message can correctly reference two packages with the same leaf-name + +message Test { + users.v1.User user = 1; + posts.v1.Post post = 2; +} diff --git a/betterproto/tests/inputs/import_packages_same_name/posts_v1.proto b/betterproto/tests/inputs/import_packages_same_name/posts_v1.proto new file mode 100644 index 0000000..506bf11 --- /dev/null +++ b/betterproto/tests/inputs/import_packages_same_name/posts_v1.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +package posts.v1; + +message Post { + +} diff --git a/betterproto/tests/inputs/import_packages_same_name/users_v1.proto b/betterproto/tests/inputs/import_packages_same_name/users_v1.proto new file mode 100644 index 0000000..6e46ba8 --- /dev/null +++ b/betterproto/tests/inputs/import_packages_same_name/users_v1.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +package users.v1; + +message User { + +} diff --git a/betterproto/tests/inputs/import_service_input_message/import_service_input_message.proto b/betterproto/tests/inputs/import_service_input_message/import_service_input_message.proto new file mode 100644 index 0000000..a5073db --- /dev/null +++ b/betterproto/tests/inputs/import_service_input_message/import_service_input_message.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +import "request_message.proto"; + +// Tests generated service correctly imports the RequestMessage + +service Test { + rpc DoThing (RequestMessage) returns (RequestResponse); +} + + +message RequestResponse { + int32 value = 1; +} + diff --git a/betterproto/tests/inputs/import_service_input_message/request_message.proto b/betterproto/tests/inputs/import_service_input_message/request_message.proto new file mode 100644 index 0000000..5bfceed --- /dev/null +++ b/betterproto/tests/inputs/import_service_input_message/request_message.proto @@ -0,0 +1,5 @@ +syntax = "proto3"; + +message RequestMessage { + int32 argument = 1; +} \ No newline at end of file diff --git a/betterproto/tests/inputs/import_service_input_message/test_import_service.py b/betterproto/tests/inputs/import_service_input_message/test_import_service.py new file mode 100644 index 0000000..a8395fc --- /dev/null +++ b/betterproto/tests/inputs/import_service_input_message/test_import_service.py @@ -0,0 +1,16 @@ +import pytest + +from betterproto.tests.mocks import MockChannel +from betterproto.tests.output_betterproto.import_service_input_message.import_service_input_message import ( + RequestResponse, + TestStub, +) + + +@pytest.mark.xfail(reason="#68 Request Input Messages are not imported for service") +@pytest.mark.asyncio +async def test_service_correctly_imports_reference_message(): + mock_response = RequestResponse(value=10) + service = TestStub(MockChannel([mock_response])) + response = await service.do_thing() + assert mock_response == response diff --git a/betterproto/tests/inputs/keywords/keywords.json b/betterproto/tests/inputs/keywords/keywords.json deleted file mode 100644 index c4f7b0c..0000000 --- a/betterproto/tests/inputs/keywords/keywords.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "for": 1, - "with": 2, - "as": 3 -} diff --git a/betterproto/tests/inputs/keywords/keywords.proto b/betterproto/tests/inputs/keywords/keywords.proto deleted file mode 100644 index 57c998b..0000000 --- a/betterproto/tests/inputs/keywords/keywords.proto +++ /dev/null @@ -1,11 +0,0 @@ -syntax = "proto3"; - -message Test { - int32 for = 1; - int32 with = 2; - int32 as = 3; -} - -service TestService { - rpc GetTest(Test) returns (Test) {} -} diff --git a/betterproto/tests/inputs/namespace_builtin_types/namespace_builtin_types.json b/betterproto/tests/inputs/namespace_builtin_types/namespace_builtin_types.json new file mode 100644 index 0000000..8200032 --- /dev/null +++ b/betterproto/tests/inputs/namespace_builtin_types/namespace_builtin_types.json @@ -0,0 +1,16 @@ +{ + "int": "value-for-int", + "float": "value-for-float", + "complex": "value-for-complex", + "list": "value-for-list", + "tuple": "value-for-tuple", + "range": "value-for-range", + "str": "value-for-str", + "bytearray": "value-for-bytearray", + "bytes": "value-for-bytes", + "memoryview": "value-for-memoryview", + "set": "value-for-set", + "frozenset": "value-for-frozenset", + "map": "value-for-map", + "bool": "value-for-bool" +} \ No newline at end of file diff --git a/betterproto/tests/inputs/namespace_builtin_types/namespace_builtin_types.proto b/betterproto/tests/inputs/namespace_builtin_types/namespace_builtin_types.proto new file mode 100644 index 0000000..636bb55 --- /dev/null +++ b/betterproto/tests/inputs/namespace_builtin_types/namespace_builtin_types.proto @@ -0,0 +1,38 @@ +syntax = "proto3"; + +// Tests that messages may contain fields with names that are python types + +message Test { + // https://docs.python.org/2/library/stdtypes.html#numeric-types-int-float-long-complex + string int = 1; + string float = 2; + string complex = 3; + + // https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range + string list = 4; + string tuple = 5; + string range = 6; + + // https://docs.python.org/3/library/stdtypes.html#str + string str = 7; + + // https://docs.python.org/3/library/stdtypes.html#bytearray-objects + string bytearray = 8; + + // https://docs.python.org/3/library/stdtypes.html#bytes-and-bytearray-operations + string bytes = 9; + + // https://docs.python.org/3/library/stdtypes.html#memory-views + string memoryview = 10; + + // https://docs.python.org/3/library/stdtypes.html#set-types-set-frozenset + string set = 11; + string frozenset = 12; + + // https://docs.python.org/3/library/stdtypes.html#dict + string map = 13; + string dict = 14; + + // https://docs.python.org/3/library/stdtypes.html#boolean-values + string bool = 15; +} \ No newline at end of file diff --git a/betterproto/tests/inputs/namespace_keywords/namespace_keywords.json b/betterproto/tests/inputs/namespace_keywords/namespace_keywords.json new file mode 100644 index 0000000..4f11b60 --- /dev/null +++ b/betterproto/tests/inputs/namespace_keywords/namespace_keywords.json @@ -0,0 +1,37 @@ +{ + "False": 1, + "None": 2, + "True": 3, + "and": 4, + "as": 5, + "assert": 6, + "async": 7, + "await": 8, + "break": 9, + "class": 10, + "continue": 11, + "def": 12, + "del": 13, + "elif": 14, + "else": 15, + "except": 16, + "finally": 17, + "for": 18, + "from": 19, + "global": 20, + "if": 21, + "import": 22, + "in": 23, + "is": 24, + "lambda": 25, + "nonlocal": 26, + "not": 27, + "or": 28, + "pass": 29, + "raise": 30, + "return": 31, + "try": 32, + "while": 33, + "with": 34, + "yield": 35 +} diff --git a/betterproto/tests/inputs/namespace_keywords/namespace_keywords.proto b/betterproto/tests/inputs/namespace_keywords/namespace_keywords.proto new file mode 100644 index 0000000..6d1a7c5 --- /dev/null +++ b/betterproto/tests/inputs/namespace_keywords/namespace_keywords.proto @@ -0,0 +1,44 @@ +syntax = "proto3"; + +// Tests that messages may contain fields that are Python keywords +// +// Generated with Python 3.7.6 +// print('\n'.join(f'string {k} = {i+1};' for i,k in enumerate(keyword.kwlist))) + +message Test { + string False = 1; + string None = 2; + string True = 3; + string and = 4; + string as = 5; + string assert = 6; + string async = 7; + string await = 8; + string break = 9; + string class = 10; + string continue = 11; + string def = 12; + string del = 13; + string elif = 14; + string else = 15; + string except = 16; + string finally = 17; + string for = 18; + string from = 19; + string global = 20; + string if = 21; + string import = 22; + string in = 23; + string is = 24; + string lambda = 25; + string nonlocal = 26; + string not = 27; + string or = 28; + string pass = 29; + string raise = 30; + string return = 31; + string try = 32; + string while = 33; + string with = 34; + string yield = 35; +} \ No newline at end of file diff --git a/betterproto/tests/inputs/xfail.py b/betterproto/tests/inputs/xfail.py deleted file mode 100644 index f80f0f4..0000000 --- a/betterproto/tests/inputs/xfail.py +++ /dev/null @@ -1,10 +0,0 @@ -# Test cases that are expected to fail, e.g. unimplemented features or bug-fixes. -# Remove from list when fixed. -tests = { - "import_root_sibling", - "import_child_package_from_package", - "import_root_package_from_child", - "import_parent_package_from_child", - "import_circular_dependency", - "oneof_enum", -} diff --git a/betterproto/tests/test_inputs.py b/betterproto/tests/test_inputs.py index 1d31348..183db25 100644 --- a/betterproto/tests/test_inputs.py +++ b/betterproto/tests/test_inputs.py @@ -8,7 +8,7 @@ from typing import Set import pytest import betterproto -from betterproto.tests.inputs import xfail +from betterproto.tests.inputs import config as test_input_config from betterproto.tests.mocks import MockChannel from betterproto.tests.util import get_directories, get_test_case_json_data, inputs_path @@ -45,9 +45,8 @@ class TestCases: test_cases = TestCases( path=inputs_path, - # test cases for services - services={"googletypes_response", "googletypes_response_embedded", "service"}, - xfail=xfail.tests, + services=test_input_config.services, + xfail=test_input_config.tests, ) plugin_output_package = "betterproto.tests.output_betterproto" diff --git a/betterproto/tests/util.py b/betterproto/tests/util.py index 11d5052..a7cff7a 100644 --- a/betterproto/tests/util.py +++ b/betterproto/tests/util.py @@ -36,10 +36,11 @@ def read_relative(file: str, path: str): return fh.read() -def protoc_plugin(path: str, output_dir: str): - subprocess.run( +def protoc_plugin(path: str, output_dir: str) -> subprocess.CompletedProcess: + return subprocess.run( f"protoc --plugin=protoc-gen-custom={plugin_path} --custom_out={output_dir} --proto_path={path} {path}/*.proto", shell=True, + check=True, )