Skip to content

Commit f6af077

Browse files
authored
Merge pull request #51 from boukeversteegh/pr/refactor-tests
Reorganize tests and add some extra documentation.
2 parents 3546f55 + 92088eb commit f6af077

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+381
-118
lines changed

.gitignore

+2-5
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,9 @@
44
.pytest_cache
55
.python-version
66
build/
7-
betterproto/tests/*.bin
8-
betterproto/tests/*_pb2.py
9-
betterproto/tests/*.py
10-
!betterproto/tests/generate.py
11-
!betterproto/tests/test_*.py
7+
betterproto/tests/output_*
128
**/__pycache__
139
dist
1410
**/*.egg-info
1511
output
12+
.idea

README.md

+23-4

betterproto/plugin.bat

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@SET plugin_dir=%~dp0
2+
@python %plugin_dir%/plugin.py %*

betterproto/tests/README.md

+75

betterproto/tests/generate.py

+41-65
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,60 @@
11
#!/usr/bin/env python
22
import os
3+
import sys
4+
from typing import Set
5+
6+
from betterproto.tests.util import get_directories, inputs_path, output_path_betterproto, output_path_reference, \
7+
protoc_plugin, protoc_reference
38

49
# Force pure-python implementation instead of C++, otherwise imports
510
# break things because we can't properly reset the symbol database.
611
os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python"
712

8-
import importlib
9-
import json
10-
import subprocess
11-
import sys
12-
from typing import Generator, Tuple
1313

14-
from google.protobuf import symbol_database
15-
from google.protobuf.descriptor_pool import DescriptorPool
16-
from google.protobuf.json_format import MessageToJson, Parse
14+
def generate(whitelist: Set[str]):
15+
path_whitelist = {os.path.realpath(e) for e in whitelist if os.path.exists(e)}
16+
name_whitelist = {e for e in whitelist if not os.path.exists(e)}
17+
18+
test_case_names = set(get_directories(inputs_path))
19+
20+
for test_case_name in sorted(test_case_names):
21+
test_case_path = os.path.realpath(os.path.join(inputs_path, test_case_name))
22+
23+
if whitelist and test_case_path not in path_whitelist and test_case_name not in name_whitelist:
24+
continue
1725

26+
case_output_dir_reference = os.path.join(output_path_reference, test_case_name)
27+
case_output_dir_betterproto = os.path.join(output_path_betterproto, test_case_name)
1828

19-
root = os.path.dirname(os.path.realpath(__file__))
29+
print(f'Generating output for {test_case_name}')
30+
os.makedirs(case_output_dir_reference, exist_ok=True)
31+
os.makedirs(case_output_dir_betterproto, exist_ok=True)
2032

33+
protoc_reference(test_case_path, case_output_dir_reference)
34+
protoc_plugin(test_case_path, case_output_dir_betterproto)
2135

22-
def get_files(end: str) -> Generator[str, None, None]:
23-
for r, dirs, files in os.walk(root):
24-
for filename in [f for f in files if f.endswith(end)]:
25-
yield os.path.join(r, filename)
2636

37+
HELP = "\n".join([
38+
'Usage: python generate.py',
39+
' python generate.py [DIRECTORIES or NAMES]',
40+
'Generate python classes for standard tests.',
41+
'',
42+
'DIRECTORIES One or more relative or absolute directories of test-cases to generate classes for.',
43+
' python generate.py inputs/bool inputs/double inputs/enum',
44+
'',
45+
'NAMES One or more test-case names to generate classes for.',
46+
' python generate.py bool double enums'
47+
])
2748

28-
def get_base(filename: str) -> str:
29-
return os.path.splitext(os.path.basename(filename))[0]
3049

50+
def main():
51+
if set(sys.argv).intersection({'-h', '--help'}):
52+
print(HELP)
53+
return
54+
whitelist = set(sys.argv[1:])
3155

32-
def ensure_ext(filename: str, ext: str) -> str:
33-
if not filename.endswith(ext):
34-
return filename + ext
35-
return filename
56+
generate(whitelist)
3657

3758

3859
if __name__ == "__main__":
39-
os.chdir(root)
40-
41-
if len(sys.argv) > 1:
42-
proto_files = [ensure_ext(f, ".proto") for f in sys.argv[1:]]
43-
bases = {get_base(f) for f in proto_files}
44-
json_files = [
45-
f for f in get_files(".json") if get_base(f).split("-")[0] in bases
46-
]
47-
else:
48-
proto_files = get_files(".proto")
49-
json_files = get_files(".json")
50-
51-
for filename in proto_files:
52-
print(f"Generating code for {os.path.basename(filename)}")
53-
subprocess.run(
54-
f"protoc --python_out=. {os.path.basename(filename)}", shell=True
55-
)
56-
subprocess.run(
57-
f"protoc --plugin=protoc-gen-custom=../plugin.py --custom_out=. {os.path.basename(filename)}",
58-
shell=True,
59-
)
60-
61-
for filename in json_files:
62-
# Reset the internal symbol database so we can import the `Test` message
63-
# multiple times. Ugh.
64-
sym = symbol_database.Default()
65-
sym.pool = DescriptorPool()
66-
67-
parts = get_base(filename).split("-")
68-
out = filename.replace(".json", ".bin")
69-
print(f"Using {parts[0]}_pb2 to generate {os.path.basename(out)}")
70-
71-
imported = importlib.import_module(f"{parts[0]}_pb2")
72-
input_json = open(filename).read()
73-
parsed = Parse(input_json, imported.Test())
74-
serialized = parsed.SerializeToString()
75-
preserve = "casing" not in filename
76-
serialized_json = MessageToJson(parsed, preserving_proto_field_name=preserve)
77-
78-
s_loaded = json.loads(serialized_json)
79-
in_loaded = json.loads(input_json)
80-
81-
if s_loaded != in_loaded:
82-
raise AssertionError("Expected JSON to be equal:", s_loaded, in_loaded)
83-
84-
open(out, "wb").write(serialized)
60+
main()
File renamed without changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from betterproto.tests.output_betterproto.bool.bool import Test
2+
3+
4+
def test_value():
5+
message = Test()
6+
assert not message.value, "Boolean is False by default"

betterproto/tests/casing.proto renamed to betterproto/tests/inputs/casing/casing.proto

+5
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,9 @@ enum my_enum {
99
message Test {
1010
int32 camelCase = 1;
1111
my_enum snake_case = 2;
12+
snake_case_message snake_case_message = 3;
1213
}
14+
15+
message snake_case_message {
16+
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import betterproto.tests.output_betterproto.casing.casing as casing
2+
from betterproto.tests.output_betterproto.casing.casing import Test
3+
4+
5+
def test_message_attributes():
6+
message = Test()
7+
assert hasattr(message, 'snake_case_message'), 'snake_case field name is same in python'
8+
assert hasattr(message, 'camel_case'), 'CamelCase field is snake_case in python'
9+
10+
11+
def test_message_casing():
12+
assert hasattr(casing, 'SnakeCaseMessage'), 'snake_case Message name is converted to CamelCase in python'
13+
14+
15+
def test_enum_casing():
16+
assert hasattr(casing, 'MyEnum'), 'snake_case Enum name is converted to CamelCase in python'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
syntax = "proto3";
2+
3+
import "google/protobuf/wrappers.proto";
4+
5+
service Test {
6+
rpc GetInt32 (Input) returns (google.protobuf.Int32Value);
7+
rpc GetAnotherInt32 (Input) returns (google.protobuf.Int32Value);
8+
rpc GetInt64 (Input) returns (google.protobuf.Int64Value);
9+
rpc GetOutput (Input) returns (Output);
10+
}
11+
12+
message Input {
13+
14+
}
15+
16+
message Output {
17+
google.protobuf.Int64Value int64 = 1;
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from typing import Optional
2+
3+
import pytest
4+
5+
from betterproto.tests.output_betterproto.googletypes_response.googletypes_response import TestStub
6+
7+
8+
class TestStubChild(TestStub):
9+
async def _unary_unary(self, route, request, response_type, **kwargs):
10+
self.response_type = response_type
11+
12+
13+
@pytest.mark.asyncio
14+
async def test():
15+
pytest.skip('todo')
16+
stub = TestStubChild(None)
17+
await stub.get_int64()
18+
assert stub.response_type != Optional[int]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"positive": 150,
3+
"negative": -150
4+
}

betterproto/tests/int32.proto renamed to betterproto/tests/inputs/int32/int32.proto

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ syntax = "proto3";
33
// Some documentation about the Test message.
44
message Test {
55
// Some documentation about the count.
6-
int32 count = 1;
6+
int32 positive = 1;
7+
int32 negative = 2;
78
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
syntax = "proto3";
2+
3+
package repeatedmessage;
4+
5+
message Test {
6+
repeated Sub greetings = 1;
7+
}
8+
9+
message Sub {
10+
string greeting = 1;
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"signed32": 150,
3+
"negative32": -150,
4+
"string64": "150",
5+
"negative64": "-150"
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
syntax = "proto3";
2+
3+
message Test {
4+
// todo: rename fields after fixing bug where 'signed_32_positive' will map to 'signed_32Positive' as output json
5+
sint32 signed32 = 1; // signed_32_positive
6+
sint32 negative32 = 2; // signed_32_negative
7+
sint64 string64 = 3; // signed_64_positive
8+
sint64 negative64 = 4; // signed_64_negative
9+
}

betterproto/tests/int32-negative.json

-3
This file was deleted.

betterproto/tests/int32.json

-3
This file was deleted.

0 commit comments

Comments
 (0)