Move betterproto/tests → tests

This commit is contained in:
Arun Babu Neelicattu
2020-07-10 13:45:52 +02:00
committed by Bouke Versteegh
parent 8864f4fdbd
commit cebf9176a3
112 changed files with 27 additions and 27 deletions

91
tests/README.md Normal file
View File

@@ -0,0 +1,91 @@
# Standard Tests Development Guide
Standard test cases are found in [betterproto/tests/inputs](inputs), where each subdirectory represents a testcase, that is verified in isolation.
```
inputs/
bool/
double/
int32/
...
```
## Test case directory structure
Each testcase has a `<name>.proto` file with a message called `Test`, and optionally a matching `.json` file and a custom test called `test_*.py`.
```bash
bool/
bool.proto
bool.json # optional
test_bool.py # optional
```
### proto
`<name>.proto` &mdash; *The protobuf message to test*
```protobuf
syntax = "proto3";
message Test {
bool value = 1;
}
```
You can add multiple `.proto` files to the test case, as long as one file matches the directory name.
### json
`<name>.json` &mdash; *Test-data to validate the message with*
```json
{
"value": true
}
```
### pytest
`test_<name>.py` &mdash; *Custom test to validate specific aspects of the generated class*
```python
from tests.output_betterproto.bool.bool import Test
def test_value():
message = Test()
assert not message.value, "Boolean is False by default"
```
## Standard tests
The following tests are automatically executed for all cases:
- [x] Can the generated python code be imported?
- [x] Can the generated message class be instantiated?
- [x] Is the generated code compatible with the Google's `grpc_tools.protoc` implementation?
- _when `.json` is present_
## Running the tests
- `pipenv run generate`
This generates:
- `betterproto/tests/output_betterproto` &mdash; *the plugin generated python classes*
- `betterproto/tests/output_reference` &mdash; *reference implementation classes*
- `pipenv run test`
## Intentionally Failing tests
The standard test suite includes tests that fail by intention. These tests document known bugs and missing features that are intended to be corrected in the future.
When running `pytest`, they show up as `x` or `X` in the test results.
```
betterproto/tests/test_inputs.py ..x...x..x...x.X........xx........x.....x.......x.xx....x...................... [ 84%]
```
- `.` &mdash; PASSED
- `x` &mdash; XFAIL: expected failure
- `X` &mdash; XPASS: expected failure, but still passed
Test cases marked for expected failure are declared in [inputs/config.py](inputs/config.py)

0
tests/__init__.py Normal file
View File

12
tests/conftest.py Normal file
View File

@@ -0,0 +1,12 @@
import pytest
def pytest_addoption(parser):
parser.addoption(
"--repeat", type=int, default=1, help="repeat the operation multiple times"
)
@pytest.fixture(scope="session")
def repeat(request):
return request.config.getoption("repeat")

141
tests/generate.py Executable file
View File

@@ -0,0 +1,141 @@
#!/usr/bin/env python
import asyncio
import os
from pathlib import Path
import shutil
import sys
from typing import Set
from tests.util import (
get_directories,
inputs_path,
output_path_betterproto,
output_path_reference,
protoc,
)
# Force pure-python implementation instead of C++, otherwise imports
# break things because we can't properly reset the symbol database.
os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python"
def clear_directory(dir_path: Path):
for file_or_directory in dir_path.glob("*"):
if file_or_directory.is_dir():
shutil.rmtree(file_or_directory)
else:
file_or_directory.unlink()
async def generate(whitelist: Set[str], verbose: bool):
test_case_names = set(get_directories(inputs_path)) - {"__pycache__"}
path_whitelist = set()
name_whitelist = set()
for item in whitelist:
if item in test_case_names:
name_whitelist.add(item)
continue
path_whitelist.add(item)
generation_tasks = []
for test_case_name in sorted(test_case_names):
test_case_input_path = inputs_path.joinpath(test_case_name).resolve()
if (
whitelist
and str(test_case_input_path) not in path_whitelist
and test_case_name not in name_whitelist
):
continue
generation_tasks.append(
generate_test_case_output(test_case_input_path, test_case_name, verbose)
)
failed_test_cases = []
# Wait for all subprocs and match any failures to names to report
for test_case_name, result in zip(
sorted(test_case_names), await asyncio.gather(*generation_tasks)
):
if result != 0:
failed_test_cases.append(test_case_name)
if failed_test_cases:
sys.stderr.write(
"\n\033[31;1;4mFailed to generate the following test cases:\033[0m\n"
)
for failed_test_case in failed_test_cases:
sys.stderr.write(f"- {failed_test_case}\n")
async def generate_test_case_output(
test_case_input_path: Path, test_case_name: str, verbose: bool
) -> int:
"""
Returns the max of the subprocess return values
"""
test_case_output_path_reference = output_path_reference.joinpath(test_case_name)
test_case_output_path_betterproto = output_path_betterproto.joinpath(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)
(
(ref_out, ref_err, ref_code),
(plg_out, plg_err, plg_code),
) = await asyncio.gather(
protoc(test_case_input_path, test_case_output_path_reference, True),
protoc(test_case_input_path, test_case_output_path_betterproto, False),
)
message = f"Generated output for {test_case_name!r}"
if verbose:
print(f"\033[31;1;4m{message}\033[0m")
if ref_out:
sys.stdout.buffer.write(ref_out)
if ref_err:
sys.stderr.buffer.write(ref_err)
if plg_out:
sys.stdout.buffer.write(plg_out)
if plg_err:
sys.stderr.buffer.write(plg_err)
sys.stdout.buffer.flush()
sys.stderr.buffer.flush()
else:
print(message)
return max(ref_code, plg_code)
HELP = "\n".join(
(
"Usage: python generate.py [-h] [-v] [DIRECTORIES or NAMES]",
"Generate python classes for standard tests.",
"",
"DIRECTORIES One or more relative or absolute directories of test-cases to generate classes for.",
" python generate.py inputs/bool inputs/double inputs/enum",
"",
"NAMES One or more test-case names to generate classes for.",
" python generate.py bool double enums",
)
)
def main():
if set(sys.argv).intersection({"-h", "--help"}):
print(HELP)
return
if sys.argv[1:2] == ["-v"]:
verbose = True
whitelist = set(sys.argv[2:])
else:
verbose = False
whitelist = set(sys.argv[1:])
asyncio.get_event_loop().run_until_complete(generate(whitelist, verbose))
if __name__ == "__main__":
main()

0
tests/grpc/__init__.py Normal file
View File

View File

@@ -0,0 +1,153 @@
import asyncio
from tests.output_betterproto.service.service import (
DoThingResponse,
DoThingRequest,
GetThingRequest,
TestStub as ThingServiceClient,
)
import grpclib
from grpclib.testing import ChannelFor
import pytest
from betterproto.grpc.util.async_channel import AsyncChannel
from .thing_service import ThingService
async def _test_client(client, name="clean room", **kwargs):
response = await client.do_thing(name=name)
assert response.names == [name]
def _assert_request_meta_received(deadline, metadata):
def server_side_test(stream):
assert stream.deadline._timestamp == pytest.approx(
deadline._timestamp, 1
), "The provided deadline should be received serverside"
assert (
stream.metadata["authorization"] == metadata["authorization"]
), "The provided authorization metadata should be received serverside"
return server_side_test
@pytest.mark.asyncio
async def test_simple_service_call():
async with ChannelFor([ThingService()]) as channel:
await _test_client(ThingServiceClient(channel))
@pytest.mark.asyncio
async def test_service_call_with_upfront_request_params():
# Setting deadline
deadline = grpclib.metadata.Deadline.from_timeout(22)
metadata = {"authorization": "12345"}
async with ChannelFor(
[ThingService(test_hook=_assert_request_meta_received(deadline, metadata),)]
) as channel:
await _test_client(
ThingServiceClient(channel, deadline=deadline, metadata=metadata)
)
# Setting timeout
timeout = 99
deadline = grpclib.metadata.Deadline.from_timeout(timeout)
metadata = {"authorization": "12345"}
async with ChannelFor(
[ThingService(test_hook=_assert_request_meta_received(deadline, metadata),)]
) as channel:
await _test_client(
ThingServiceClient(channel, timeout=timeout, metadata=metadata)
)
@pytest.mark.asyncio
async def test_service_call_lower_level_with_overrides():
THING_TO_DO = "get milk"
# Setting deadline
deadline = grpclib.metadata.Deadline.from_timeout(22)
metadata = {"authorization": "12345"}
kwarg_deadline = grpclib.metadata.Deadline.from_timeout(28)
kwarg_metadata = {"authorization": "12345"}
async with ChannelFor(
[ThingService(test_hook=_assert_request_meta_received(deadline, metadata),)]
) as channel:
client = ThingServiceClient(channel, deadline=deadline, metadata=metadata)
response = await client._unary_unary(
"/service.Test/DoThing",
DoThingRequest(THING_TO_DO),
DoThingResponse,
deadline=kwarg_deadline,
metadata=kwarg_metadata,
)
assert response.names == [THING_TO_DO]
# Setting timeout
timeout = 99
deadline = grpclib.metadata.Deadline.from_timeout(timeout)
metadata = {"authorization": "12345"}
kwarg_timeout = 9000
kwarg_deadline = grpclib.metadata.Deadline.from_timeout(kwarg_timeout)
kwarg_metadata = {"authorization": "09876"}
async with ChannelFor(
[
ThingService(
test_hook=_assert_request_meta_received(kwarg_deadline, kwarg_metadata),
)
]
) as channel:
client = ThingServiceClient(channel, deadline=deadline, metadata=metadata)
response = await client._unary_unary(
"/service.Test/DoThing",
DoThingRequest(THING_TO_DO),
DoThingResponse,
timeout=kwarg_timeout,
metadata=kwarg_metadata,
)
assert response.names == [THING_TO_DO]
@pytest.mark.asyncio
async def test_async_gen_for_unary_stream_request():
thing_name = "my milkshakes"
async with ChannelFor([ThingService()]) as channel:
client = ThingServiceClient(channel)
expected_versions = [5, 4, 3, 2, 1]
async for response in client.get_thing_versions(name=thing_name):
assert response.name == thing_name
assert response.version == expected_versions.pop()
@pytest.mark.asyncio
async def test_async_gen_for_stream_stream_request():
some_things = ["cake", "cricket", "coral reef"]
more_things = ["ball", "that", "56kmodem", "liberal humanism", "cheesesticks"]
expected_things = (*some_things, *more_things)
async with ChannelFor([ThingService()]) as channel:
client = ThingServiceClient(channel)
# Use an AsyncChannel to decouple sending and recieving, it'll send some_things
# immediately and we'll use it to send more_things later, after recieving some
# results
request_chan = AsyncChannel()
send_initial_requests = asyncio.ensure_future(
request_chan.send_from(GetThingRequest(name) for name in some_things)
)
response_index = 0
async for response in client.get_different_things(request_chan):
assert response.name == expected_things[response_index]
assert response.version == response_index + 1
response_index += 1
if more_things:
# Send some more requests as we receive responses to be sure coordination of
# send/receive events doesn't matter
await request_chan.send(GetThingRequest(more_things.pop(0)))
elif not send_initial_requests.done():
# Make sure the sending task it completed
await send_initial_requests
else:
# No more things to send make sure channel is closed
request_chan.close()
assert response_index == len(
expected_things
), "Didn't receive all expected responses"

View File

@@ -0,0 +1,100 @@
import asyncio
import betterproto
from betterproto.grpc.util.async_channel import AsyncChannel
from dataclasses import dataclass
import pytest
from typing import AsyncIterator
@dataclass
class Message(betterproto.Message):
body: str = betterproto.string_field(1)
@pytest.fixture
def expected_responses():
return [Message("Hello world 1"), Message("Hello world 2"), Message("Done")]
class ClientStub:
async def connect(self, requests: AsyncIterator):
await asyncio.sleep(0.1)
async for request in requests:
await asyncio.sleep(0.1)
yield request
await asyncio.sleep(0.1)
yield Message("Done")
async def to_list(generator: AsyncIterator):
result = []
async for value in generator:
result.append(value)
return result
@pytest.fixture
def client():
# channel = Channel(host='127.0.0.1', port=50051)
# return ClientStub(channel)
return ClientStub()
@pytest.mark.asyncio
async def test_send_from_before_connect_and_close_automatically(
client, expected_responses
):
requests = AsyncChannel()
await requests.send_from(
[Message(body="Hello world 1"), Message(body="Hello world 2")], close=True
)
responses = client.connect(requests)
assert await to_list(responses) == expected_responses
@pytest.mark.asyncio
async def test_send_from_after_connect_and_close_automatically(
client, expected_responses
):
requests = AsyncChannel()
responses = client.connect(requests)
await requests.send_from(
[Message(body="Hello world 1"), Message(body="Hello world 2")], close=True
)
assert await to_list(responses) == expected_responses
@pytest.mark.asyncio
async def test_send_from_close_manually_immediately(client, expected_responses):
requests = AsyncChannel()
responses = client.connect(requests)
await requests.send_from(
[Message(body="Hello world 1"), Message(body="Hello world 2")], close=False
)
requests.close()
assert await to_list(responses) == expected_responses
@pytest.mark.asyncio
async def test_send_individually_and_close_before_connect(client, expected_responses):
requests = AsyncChannel()
await requests.send(Message(body="Hello world 1"))
await requests.send(Message(body="Hello world 2"))
requests.close()
responses = client.connect(requests)
assert await to_list(responses) == expected_responses
@pytest.mark.asyncio
async def test_send_individually_and_close_after_connect(client, expected_responses):
requests = AsyncChannel()
await requests.send(Message(body="Hello world 1"))
await requests.send(Message(body="Hello world 2"))
responses = client.connect(requests)
requests.close()
assert await to_list(responses) == expected_responses

View File

@@ -0,0 +1,83 @@
from tests.output_betterproto.service.service import (
DoThingResponse,
DoThingRequest,
GetThingRequest,
GetThingResponse,
)
import grpclib
import grpclib.server
from typing import Dict
class ThingService:
def __init__(self, test_hook=None):
# This lets us pass assertions to the servicer ;)
self.test_hook = test_hook
async def do_thing(
self, stream: "grpclib.server.Stream[DoThingRequest, DoThingResponse]"
):
request = await stream.recv_message()
if self.test_hook is not None:
self.test_hook(stream)
await stream.send_message(DoThingResponse([request.name]))
async def do_many_things(
self, stream: "grpclib.server.Stream[DoThingRequest, DoThingResponse]"
):
thing_names = [request.name for request in stream]
if self.test_hook is not None:
self.test_hook(stream)
await stream.send_message(DoThingResponse(thing_names))
async def get_thing_versions(
self, stream: "grpclib.server.Stream[GetThingRequest, GetThingResponse]"
):
request = await stream.recv_message()
if self.test_hook is not None:
self.test_hook(stream)
for version_num in range(1, 6):
await stream.send_message(
GetThingResponse(name=request.name, version=version_num)
)
async def get_different_things(
self, stream: "grpclib.server.Stream[GetThingRequest, GetThingResponse]"
):
if self.test_hook is not None:
self.test_hook(stream)
# Respond to each input item immediately
response_num = 0
async for request in stream:
response_num += 1
await stream.send_message(
GetThingResponse(name=request.name, version=response_num)
)
def __mapping__(self) -> Dict[str, "grpclib.const.Handler"]:
return {
"/service.Test/DoThing": grpclib.const.Handler(
self.do_thing,
grpclib.const.Cardinality.UNARY_UNARY,
DoThingRequest,
DoThingResponse,
),
"/service.Test/DoManyThings": grpclib.const.Handler(
self.do_many_things,
grpclib.const.Cardinality.STREAM_UNARY,
DoThingRequest,
DoThingResponse,
),
"/service.Test/GetThingVersions": grpclib.const.Handler(
self.get_thing_versions,
grpclib.const.Cardinality.UNARY_STREAM,
GetThingRequest,
GetThingResponse,
),
"/service.Test/GetDifferentThings": grpclib.const.Handler(
self.get_different_things,
grpclib.const.Cardinality.STREAM_STREAM,
GetThingRequest,
GetThingResponse,
),
}

View File

@@ -0,0 +1,3 @@
{
"value": true
}

View File

@@ -0,0 +1,5 @@
syntax = "proto3";
message Test {
bool value = 1;
}

View File

@@ -0,0 +1,6 @@
from tests.output_betterproto.bool import Test
def test_value():
message = Test()
assert not message.value, "Boolean is False by default"

View File

@@ -0,0 +1,3 @@
{
"data": "SGVsbG8sIFdvcmxkIQ=="
}

View File

@@ -0,0 +1,5 @@
syntax = "proto3";
message Test {
bytes data = 1;
}

View File

@@ -0,0 +1,4 @@
{
"camelCase": 1,
"snakeCase": "ONE"
}

View File

@@ -0,0 +1,18 @@
syntax = "proto3";
enum my_enum {
ZERO = 0;
ONE = 1;
TWO = 2;
}
message Test {
int32 camelCase = 1;
my_enum snake_case = 2;
snake_case_message snake_case_message = 3;
int32 UPPERCASE = 4;
}
message snake_case_message {
}

View File

@@ -0,0 +1,23 @@
import tests.output_betterproto.casing as casing
from tests.output_betterproto.casing import Test
def test_message_attributes():
message = Test()
assert hasattr(
message, "snake_case_message"
), "snake_case field name is same in python"
assert hasattr(message, "camel_case"), "CamelCase field is snake_case in python"
assert hasattr(message, "uppercase"), "UPPERCASE field is lowercase in python"
def test_message_casing():
assert hasattr(
casing, "SnakeCaseMessage"
), "snake_case Message name is converted to CamelCase in python"
def test_enum_casing():
assert hasattr(
casing, "MyEnum"
), "snake_case Enum name is converted to CamelCase in python"

View File

@@ -0,0 +1,7 @@
syntax = "proto3";
message Test {
int32 UPPERCASE = 1;
int32 UPPERCASE_V2 = 2;
int32 UPPER_CAMEL_CASE = 3;
}

View File

@@ -0,0 +1,14 @@
from tests.output_betterproto.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"

20
tests/inputs/config.py Normal file
View File

@@ -0,0 +1,20 @@
# Test cases that are expected to fail, e.g. unimplemented features or bug-fixes.
# Remove from list when fixed.
xfail = {
"oneof_enum", # 63
"namespace_keywords", # 70
"namespace_builtin_types", # 53
"googletypes_struct", # 9
"googletypes_value", # 9,
"import_capitalized_package",
"example", # This is the example in the readme. Not a test.
}
services = {
"googletypes_response",
"googletypes_response_embedded",
"service",
"import_service_input_message",
"googletypes_service_returns_empty",
"googletypes_service_returns_googletype",
}

View File

@@ -0,0 +1,3 @@
{
"count": -123.45
}

View File

@@ -0,0 +1,3 @@
{
"count": 123.45
}

View File

@@ -0,0 +1,5 @@
syntax = "proto3";
message Test {
double count = 1;
}

View File

@@ -0,0 +1,3 @@
{
"greeting": "HEY"
}

View File

@@ -0,0 +1,14 @@
syntax = "proto3";
// Enum for the different greeting types
enum Greeting {
HI = 0;
HEY = 1;
// Formal greeting
HELLO = 2;
}
message Test {
// Greeting enum example
Greeting greeting = 1;
}

View File

@@ -0,0 +1,8 @@
syntax = "proto3";
package hello;
// Greeting represents a message you can tell a user.
message Greeting {
string message = 1;
}

View File

@@ -0,0 +1,6 @@
{
"foo": 4294967295,
"bar": -2147483648,
"baz": "18446744073709551615",
"qux": "-9223372036854775808"
}

View File

@@ -0,0 +1,8 @@
syntax = "proto3";
message Test {
fixed32 foo = 1;
sfixed32 bar = 2;
fixed64 baz = 3;
sfixed64 qux = 4;
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,7 @@
{
"maybe": false,
"ts": "1972-01-01T10:00:20.021Z",
"duration": "1.200s",
"important": 10,
"empty": {}
}

View File

@@ -0,0 +1,14 @@
syntax = "proto3";
import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
import "google/protobuf/empty.proto";
message Test {
google.protobuf.BoolValue maybe = 1;
google.protobuf.Timestamp ts = 2;
google.protobuf.Duration duration = 3;
google.protobuf.Int32Value important = 4;
google.protobuf.Empty empty = 5;
}

View File

@@ -0,0 +1,21 @@
syntax = "proto3";
import "google/protobuf/wrappers.proto";
// Tests that wrapped values can be used directly as return values
service Test {
rpc GetDouble (Input) returns (google.protobuf.DoubleValue);
rpc GetFloat (Input) returns (google.protobuf.FloatValue);
rpc GetInt64 (Input) returns (google.protobuf.Int64Value);
rpc GetUInt64 (Input) returns (google.protobuf.UInt64Value);
rpc GetInt32 (Input) returns (google.protobuf.Int32Value);
rpc GetUInt32 (Input) returns (google.protobuf.UInt32Value);
rpc GetBool (Input) returns (google.protobuf.BoolValue);
rpc GetString (Input) returns (google.protobuf.StringValue);
rpc GetBytes (Input) returns (google.protobuf.BytesValue);
}
message Input {
}

View File

@@ -0,0 +1,54 @@
from typing import Any, Callable, Optional
import betterproto.lib.google.protobuf as protobuf
import pytest
from tests.mocks import MockChannel
from tests.output_betterproto.googletypes_response import TestStub
test_cases = [
(TestStub.get_double, protobuf.DoubleValue, 2.5),
(TestStub.get_float, protobuf.FloatValue, 2.5),
(TestStub.get_int64, protobuf.Int64Value, -64),
(TestStub.get_u_int64, protobuf.UInt64Value, 64),
(TestStub.get_int32, protobuf.Int32Value, -32),
(TestStub.get_u_int32, protobuf.UInt32Value, 32),
(TestStub.get_bool, protobuf.BoolValue, True),
(TestStub.get_string, protobuf.StringValue, "string"),
(TestStub.get_bytes, protobuf.BytesValue, bytes(0xFF)[0:4]),
]
@pytest.mark.asyncio
@pytest.mark.parametrize(["service_method", "wrapper_class", "value"], test_cases)
async def test_channel_receives_wrapped_type(
service_method: Callable[[TestStub], Any], wrapper_class: Callable, value
):
wrapped_value = wrapper_class()
wrapped_value.value = value
channel = MockChannel(responses=[wrapped_value])
service = TestStub(channel)
await service_method(service)
assert channel.requests[0]["response_type"] != Optional[type(value)]
assert channel.requests[0]["response_type"] == type(wrapped_value)
@pytest.mark.asyncio
@pytest.mark.xfail
@pytest.mark.parametrize(["service_method", "wrapper_class", "value"], test_cases)
async def test_service_unwraps_response(
service_method: Callable[[TestStub], Any], wrapper_class: Callable, value
):
"""
grpclib does not unwrap wrapper values returned by services
"""
wrapped_value = wrapper_class()
wrapped_value.value = value
service = TestStub(MockChannel(responses=[wrapped_value]))
response_value = await service_method(service)
assert response_value == value
assert type(response_value) == type(value)

View File

@@ -0,0 +1,24 @@
syntax = "proto3";
import "google/protobuf/wrappers.proto";
// Tests that wrapped values are supported as part of output message
service Test {
rpc getOutput (Input) returns (Output);
}
message Input {
}
message Output {
google.protobuf.DoubleValue double_value = 1;
google.protobuf.FloatValue float_value = 2;
google.protobuf.Int64Value int64_value = 3;
google.protobuf.UInt64Value uint64_value = 4;
google.protobuf.Int32Value int32_value = 5;
google.protobuf.UInt32Value uint32_value = 6;
google.protobuf.BoolValue bool_value = 7;
google.protobuf.StringValue string_value = 8;
google.protobuf.BytesValue bytes_value = 9;
}

View File

@@ -0,0 +1,39 @@
import pytest
from tests.mocks import MockChannel
from tests.output_betterproto.googletypes_response_embedded import (
Output,
TestStub,
)
@pytest.mark.asyncio
async def test_service_passes_through_unwrapped_values_embedded_in_response():
"""
We do not not need to implement value unwrapping for embedded well-known types,
as this is already handled by grpclib. This test merely shows that this is the case.
"""
output = Output(
double_value=10.0,
float_value=12.0,
int64_value=-13,
uint64_value=14,
int32_value=-15,
uint32_value=16,
bool_value=True,
string_value="string",
bytes_value=bytes(0xFF)[0:4],
)
service = TestStub(MockChannel(responses=[output]))
response = await service.get_output()
assert response.double_value == 10.0
assert response.float_value == 12.0
assert response.int64_value == -13
assert response.uint64_value == 14
assert response.int32_value == -15
assert response.uint32_value == 16
assert response.bool_value
assert response.string_value == "string"
assert response.bytes_value == bytes(0xFF)[0:4]

View File

@@ -0,0 +1,11 @@
syntax = "proto3";
import "google/protobuf/empty.proto";
service Test {
rpc Send (RequestMessage) returns (google.protobuf.Empty) {
}
}
message RequestMessage {
}

View File

@@ -0,0 +1,16 @@
syntax = "proto3";
import "google/protobuf/empty.proto";
import "google/protobuf/struct.proto";
// Tests that imports are generated correctly when returning Google well-known types
service Test {
rpc GetEmpty (RequestMessage) returns (google.protobuf.Empty);
rpc GetStruct (RequestMessage) returns (google.protobuf.Struct);
rpc GetListValue (RequestMessage) returns (google.protobuf.ListValue);
rpc GetValue (RequestMessage) returns (google.protobuf.Value);
}
message RequestMessage {
}

View File

@@ -0,0 +1,5 @@
{
"struct": {
"key": true
}
}

View File

@@ -0,0 +1,7 @@
syntax = "proto3";
import "google/protobuf/struct.proto";
message Test {
google.protobuf.Struct struct = 1;
}

View File

@@ -0,0 +1,11 @@
{
"value1": "hello world",
"value2": true,
"value3": 1,
"value4": null,
"value5": [
1,
2,
3
]
}

View File

@@ -0,0 +1,13 @@
syntax = "proto3";
import "google/protobuf/struct.proto";
// Tests that fields of type google.protobuf.Value can contain arbitrary JSON-values.
message Test {
google.protobuf.Value value1 = 1;
google.protobuf.Value value2 = 2;
google.protobuf.Value value3 = 3;
google.protobuf.Value value4 = 4;
google.protobuf.Value value5 = 5;
}

View File

@@ -0,0 +1,8 @@
syntax = "proto3";
package Capitalized;
message Message {
}

View File

@@ -0,0 +1,9 @@
syntax = "proto3";
import "capitalized.proto";
// Tests that we can import from a package with a capital name, that looks like a nested type, but isn't.
message Test {
Capitalized.Message message = 1;
}

View File

@@ -0,0 +1,7 @@
syntax = "proto3";
package package.childpackage;
message ChildMessage {
}

View File

@@ -0,0 +1,9 @@
syntax = "proto3";
import "package_message.proto";
// Tests generated imports when a message in a package refers to a message in a nested child package.
message Test {
package.PackageMessage message = 1;
}

View File

@@ -0,0 +1,9 @@
syntax = "proto3";
import "child.proto";
package package;
message PackageMessage {
package.childpackage.ChildMessage c = 1;
}

View File

@@ -0,0 +1,7 @@
syntax = "proto3";
package childpackage;
message Message {
}

View File

@@ -0,0 +1,9 @@
syntax = "proto3";
import "child.proto";
// Tests generated imports when a message in root refers to a message in a child package.
message Test {
childpackage.Message child = 1;
}

View File

@@ -0,0 +1,28 @@
syntax = "proto3";
import "root.proto";
import "other.proto";
// This test-case verifies support for circular dependencies in the generated python files.
//
// This is important because we generate 1 python file/module per package, rather than 1 file per proto file.
//
// Scenario:
//
// The proto messages depend on each other in a non-circular way:
//
// Test -------> RootPackageMessage <--------------.
// `------------------------------------> OtherPackageMessage
//
// Test and RootPackageMessage are in different files, but belong to the same package (root):
//
// (Test -------> RootPackageMessage) <------------.
// `------------------------------------> OtherPackageMessage
//
// After grouping the packages into single files or modules, a circular dependency is created:
//
// (root: Test & RootPackageMessage) <-------> (other: OtherPackageMessage)
message Test {
RootPackageMessage message = 1;
other.OtherPackageMessage other = 2;
}

View File

@@ -0,0 +1,8 @@
syntax = "proto3";
import "root.proto";
package other;
message OtherPackageMessage {
RootPackageMessage rootPackageMessage = 1;
}

View File

@@ -0,0 +1,5 @@
syntax = "proto3";
message RootPackageMessage {
}

View File

@@ -0,0 +1,6 @@
syntax = "proto3";
package cousin.cousin_subpackage;
message CousinMessage {
}

View File

@@ -0,0 +1,11 @@
syntax = "proto3";
package test.subpackage;
import "cousin.proto";
// Verify that we can import message unrelated to us
message Test {
cousin.cousin_subpackage.CousinMessage message = 1;
}

View File

@@ -0,0 +1,6 @@
syntax = "proto3";
package cousin.subpackage;
message CousinMessage {
}

View File

@@ -0,0 +1,11 @@
syntax = "proto3";
package test.subpackage;
import "cousin.proto";
// Verify that we can import a message unrelated to us, in a subpackage with the same name as us.
message Test {
cousin.subpackage.CousinMessage message = 1;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,7 @@
syntax = "proto3";
package posts.v1;
message Post {
}

View File

@@ -0,0 +1,7 @@
syntax = "proto3";
package users.v1;
message User {
}

View File

@@ -0,0 +1,12 @@
syntax = "proto3";
import "parent_package_message.proto";
package parent.child;
// Tests generated imports when a message refers to a message defined in its parent package
message Test {
ParentPackageMessage message_implicit = 1;
parent.ParentPackageMessage message_explicit = 2;
}

View File

@@ -0,0 +1,6 @@
syntax = "proto3";
package parent;
message ParentPackageMessage {
}

View File

@@ -0,0 +1,11 @@
syntax = "proto3";
package child;
import "root.proto";
// Verify that we can import root message from child package
message Test {
RootMessage message = 1;
}

View File

@@ -0,0 +1,5 @@
syntax = "proto3";
message RootMessage {
}

View File

@@ -0,0 +1,9 @@
syntax = "proto3";
import "sibling.proto";
// Tests generated imports when a message in the root package refers to another message in the root package
message Test {
SiblingMessage sibling = 1;
}

View File

@@ -0,0 +1,5 @@
syntax = "proto3";
message SiblingMessage {
}

View File

@@ -0,0 +1,7 @@
syntax = "proto3";
package child;
message ChildRequestMessage {
int32 child_argument = 1;
}

View File

@@ -0,0 +1,23 @@
syntax = "proto3";
import "request_message.proto";
import "child_package_request_message.proto";
// Tests generated service correctly imports the RequestMessage
service Test {
rpc DoThing (RequestMessage) returns (RequestResponse);
rpc DoThing2 (child.ChildRequestMessage) returns (RequestResponse);
rpc DoThing3 (Nested.RequestMessage) returns (RequestResponse);
}
message RequestResponse {
int32 value = 1;
}
message Nested {
message RequestMessage {
int32 nestedArgument = 1;
}
}

View File

@@ -0,0 +1,5 @@
syntax = "proto3";
message RequestMessage {
int32 argument = 1;
}

View File

@@ -0,0 +1,31 @@
import pytest
from tests.mocks import MockChannel
from tests.output_betterproto.import_service_input_message import (
RequestResponse,
TestStub,
)
@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(argument=1)
assert mock_response == response
@pytest.mark.asyncio
async def test_service_correctly_imports_reference_message_from_child_package():
mock_response = RequestResponse(value=10)
service = TestStub(MockChannel([mock_response]))
response = await service.do_thing2(child_argument=1)
assert mock_response == response
@pytest.mark.asyncio
async def test_service_correctly_imports_nested_reference():
mock_response = RequestResponse(value=10)
service = TestStub(MockChannel([mock_response]))
response = await service.do_thing3(nested_argument=1)
assert mock_response == response

View File

@@ -0,0 +1,4 @@
{
"positive": 150,
"negative": -150
}

View File

@@ -0,0 +1,8 @@
syntax = "proto3";
// Some documentation about the Test message.
message Test {
// Some documentation about the count.
int32 positive = 1;
int32 negative = 2;
}

View File

@@ -0,0 +1,7 @@
{
"counts": {
"item1": 1,
"item2": 2,
"item3": 3
}
}

View File

@@ -0,0 +1,5 @@
syntax = "proto3";
message Test {
map<string, int32> counts = 1;
}

View File

@@ -0,0 +1,10 @@
{
"items": {
"foo": {
"count": 1
},
"bar": {
"count": 2
}
}
}

View File

@@ -0,0 +1,9 @@
syntax = "proto3";
message Test {
map<string, Nested> items = 1;
}
message Nested {
int32 count = 1;
}

View File

@@ -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"
}

View File

@@ -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;
}

View File

@@ -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
}

View File

@@ -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;
}

View File

@@ -0,0 +1,6 @@
{
"nested": {
"count": 150
},
"sibling": {}
}

View File

@@ -0,0 +1,18 @@
syntax = "proto3";
// A test message with a nested message inside of it.
message Test {
// This is the nested type.
message Nested {
// Stores a simple counter.
int32 count = 1;
}
Nested nested = 1;
Sibling sibling = 2;
Sibling sibling2 = 3;
}
message Sibling {
int32 foo = 1;
}

View File

@@ -0,0 +1,19 @@
syntax = "proto3";
import "package.proto";
message Game {
message Player {
enum Race {
human = 0;
orc = 1;
}
}
}
message Test {
Game game = 1;
Game.Player GamePlayer = 2;
Game.Player.Race GamePlayerRace = 3;
equipment.Weapon Weapon = 4;
}

View File

@@ -0,0 +1,7 @@
syntax = "proto3";
package equipment;
message Weapon {
}

View File

@@ -0,0 +1,11 @@
{
"top": {
"name": "double-nested",
"middle": {
"bottom": [{"foo": "hello"}],
"enumBottom": ["A"],
"topMiddleBottom": [{"a": "hello"}],
"bar": true
}
}
}

View File

@@ -0,0 +1,26 @@
syntax = "proto3";
message Test {
message Top {
message Middle {
message TopMiddleBottom {
string a = 1;
}
enum EnumBottom{
A = 0;
B = 1;
}
message Bottom {
string foo = 1;
}
reserved 1;
repeated Bottom bottom = 2;
repeated EnumBottom enumBottom=3;
repeated TopMiddleBottom topMiddleBottom=4;
bool bar = 5;
}
string name = 1;
Middle middle = 2;
}
Top top = 1;
}

View File

@@ -0,0 +1,3 @@
{
"name": "foobar"
}

View File

@@ -0,0 +1,3 @@
{
"count": 100
}

View File

@@ -0,0 +1,8 @@
syntax = "proto3";
message Test {
oneof foo {
int32 count = 1;
string name = 2;
}
}

View File

@@ -0,0 +1,15 @@
import betterproto
from tests.output_betterproto.oneof import Test
from tests.util import get_test_case_json_data
def test_which_count():
message = Test()
message.from_json(get_test_case_json_data("oneof"))
assert betterproto.which_one_of(message, "foo") == ("count", 100)
def test_which_name():
message = Test()
message.from_json(get_test_case_json_data("oneof", "oneof-name.json"))
assert betterproto.which_one_of(message, "foo") == ("name", "foobar")

View File

@@ -0,0 +1,3 @@
{
"signal": "PASS"
}

View File

@@ -0,0 +1,3 @@
{
"signal": "RESIGN"
}

View File

@@ -0,0 +1,6 @@
{
"move": {
"x": 2,
"y": 3
}
}

View File

@@ -0,0 +1,18 @@
syntax = "proto3";
message Test {
oneof action {
Signal signal = 1;
Move move = 2;
}
}
enum Signal {
PASS = 0;
RESIGN = 1;
}
message Move {
int32 x = 1;
int32 y = 2;
}

View File

@@ -0,0 +1,42 @@
import pytest
import betterproto
from tests.output_betterproto.oneof_enum import (
Move,
Signal,
Test,
)
from tests.util import get_test_case_json_data
@pytest.mark.xfail
def test_which_one_of_returns_enum_with_default_value():
"""
returns first field when it is enum and set with default value
"""
message = Test()
message.from_json(get_test_case_json_data("oneof_enum", "oneof_enum-enum-0.json"))
assert message.move is None
assert message.signal == Signal.PASS
assert betterproto.which_one_of(message, "action") == ("signal", Signal.PASS)
@pytest.mark.xfail
def test_which_one_of_returns_enum_with_non_default_value():
"""
returns first field when it is enum and set with non default value
"""
message = Test()
message.from_json(get_test_case_json_data("oneof_enum", "oneof_enum-enum-1.json"))
assert message.move is None
assert message.signal == Signal.PASS
assert betterproto.which_one_of(message, "action") == ("signal", Signal.RESIGN)
@pytest.mark.xfail
def test_which_one_of_returns_second_field_when_set():
message = Test()
message.from_json(get_test_case_json_data("oneof_enum"))
assert message.move == Move(x=2, y=3)
assert message.signal == 0
assert betterproto.which_one_of(message, "action") == ("move", Move(x=2, y=3))

View File

@@ -0,0 +1,5 @@
{
"greeting": {
"greeting": "hello"
}
}

View File

@@ -0,0 +1,7 @@
syntax = "proto3";
import "repeatedmessage.proto";
message Test {
repeatedmessage.Sub greeting = 1;
}

View File

@@ -0,0 +1,11 @@
syntax = "proto3";
package repeatedmessage;
message Test {
repeated Sub greetings = 1;
}
message Sub {
string greeting = 1;
}

View File

@@ -0,0 +1,3 @@
{
"names": ["one", "two", "three"]
}

View File

@@ -0,0 +1,5 @@
syntax = "proto3";
message Test {
repeated string names = 1;
}

View File

@@ -0,0 +1,10 @@
{
"greetings": [
{
"greeting": "hello"
},
{
"greeting": "hi"
}
]
}

View File

@@ -0,0 +1,11 @@
syntax = "proto3";
package repeatedmessage;
message Test {
repeated Sub greetings = 1;
}
message Sub {
string greeting = 1;
}

View File

@@ -0,0 +1,5 @@
{
"counts": [1, 2, -1, -2],
"signed": ["1", "2", "-1", "-2"],
"fixed": [1.0, 2.7, 3.4]
}

Some files were not shown because too many files have changed in this diff Show More