Merge pull request #55 from boukeversteegh/pr/xfail-tests

Add intentionally failing test-cases for unimplemented bug-fixes
This commit is contained in:
nat 2020-05-25 09:54:26 +02:00 committed by GitHub
commit 2f9497e064
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 353 additions and 52 deletions

View File

@ -73,3 +73,18 @@ The following tests are automatically executed for all cases:
- `betterproto/tests/output_reference` — *reference implementation classes* - `betterproto/tests/output_reference` — *reference implementation classes*
- `pipenv run test` - `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 corrented 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%]
```
- `.` — PASSED
- `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)

View File

@ -1,5 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
import glob
import os import os
import shutil
import sys import sys
from typing import Set from typing import Set
@ -17,6 +19,14 @@ from betterproto.tests.util import (
os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python" os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python"
def clear_directory(path: str):
for file_or_directory in glob.glob(os.path.join(path, "*")):
if os.path.isdir(file_or_directory):
shutil.rmtree(file_or_directory)
else:
os.remove(file_or_directory)
def generate(whitelist: Set[str]): def generate(whitelist: Set[str]):
path_whitelist = {os.path.realpath(e) for e in whitelist if os.path.exists(e)} path_whitelist = {os.path.realpath(e) for e in whitelist if os.path.exists(e)}
name_whitelist = {e for e in whitelist if not os.path.exists(e)} name_whitelist = {e for e in whitelist if not os.path.exists(e)}
@ -24,26 +34,33 @@ def generate(whitelist: Set[str]):
test_case_names = set(get_directories(inputs_path)) test_case_names = set(get_directories(inputs_path))
for test_case_name in sorted(test_case_names): for test_case_name in sorted(test_case_names):
test_case_path = os.path.realpath(os.path.join(inputs_path, test_case_name)) test_case_input_path = os.path.realpath(
os.path.join(inputs_path, test_case_name)
)
if ( if (
whitelist whitelist
and test_case_path not in path_whitelist and test_case_input_path not in path_whitelist
and test_case_name not in name_whitelist and test_case_name not in name_whitelist
): ):
continue continue
case_output_dir_reference = os.path.join(output_path_reference, test_case_name) test_case_output_path_reference = os.path.join(
case_output_dir_betterproto = os.path.join( output_path_reference, test_case_name
)
test_case_output_path_betterproto = os.path.join(
output_path_betterproto, test_case_name output_path_betterproto, test_case_name
) )
print(f"Generating output for {test_case_name}") print(f"Generating output for {test_case_name}")
os.makedirs(case_output_dir_reference, exist_ok=True) os.makedirs(test_case_output_path_reference, exist_ok=True)
os.makedirs(case_output_dir_betterproto, exist_ok=True) os.makedirs(test_case_output_path_betterproto, exist_ok=True)
protoc_reference(test_case_path, case_output_dir_reference) clear_directory(test_case_output_path_reference)
protoc_plugin(test_case_path, case_output_dir_betterproto) 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( HELP = "\n".join(

View File

@ -43,11 +43,14 @@ async def test_channel_receives_wrapped_type(
async def test_service_unwraps_response( async def test_service_unwraps_response(
service_method: Callable[[TestStub], Any], wrapper_class: Callable, value service_method: Callable[[TestStub], Any], wrapper_class: Callable, value
): ):
"""
grpclib does not unwrap wrapper values returned by services
"""
wrapped_value = wrapper_class() wrapped_value = wrapper_class()
wrapped_value.value = value wrapped_value.value = value
service = TestStub(MockChannel(responses=[wrapped_value])) service = TestStub(MockChannel(responses=[wrapped_value]))
response_value = await service_method(service) response_value = await service_method(service)
assert type(response_value) == value assert response_value == value
assert type(response_value) == type(value) assert type(response_value) == type(value)

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 that future implementations will support circular dependencies in the generated python files.
//
// This becomes important when generating 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,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";
import "root.proto";
package child;
// Tests generated imports when a message inside a child-package refers to a message defined in the root.
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

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

View File

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

View File

@ -0,0 +1,15 @@
import betterproto
from betterproto.tests.output_betterproto.oneof.oneof import Test
from betterproto.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 betterproto.tests.output_betterproto.oneof_enum.oneof_enum import (
Move,
Signal,
Test,
)
from betterproto.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

@ -10,6 +10,6 @@ message DoThingResponse {
int32 successfulIterations = 1; int32 successfulIterations = 1;
} }
service ExampleService { service Test {
rpc DoThing (DoThingRequest) returns (DoThingResponse); rpc DoThing (DoThingRequest) returns (DoThingResponse);
} }

View File

@ -7,7 +7,7 @@ from typing import Dict
from betterproto.tests.output_betterproto.service.service import ( from betterproto.tests.output_betterproto.service.service import (
DoThingResponse, DoThingResponse,
DoThingRequest, DoThingRequest,
ExampleServiceStub, TestStub as ExampleServiceStub,
) )
@ -29,12 +29,12 @@ class ExampleService:
def __mapping__(self) -> Dict[str, grpclib.const.Handler]: def __mapping__(self) -> Dict[str, grpclib.const.Handler]:
return { return {
"/service.ExampleService/DoThing": grpclib.const.Handler( "/service.Test/DoThing": grpclib.const.Handler(
self.DoThing, self.DoThing,
grpclib.const.Cardinality.UNARY_UNARY, grpclib.const.Cardinality.UNARY_UNARY,
DoThingRequest, DoThingRequest,
DoThingResponse, DoThingResponse,
) ),
} }
@ -99,7 +99,7 @@ async def test_service_call_lower_level_with_overrides():
) as channel: ) as channel:
stub = ExampleServiceStub(channel, deadline=deadline, metadata=metadata) stub = ExampleServiceStub(channel, deadline=deadline, metadata=metadata)
response = await stub._unary_unary( response = await stub._unary_unary(
"/service.ExampleService/DoThing", "/service.Test/DoThing",
DoThingRequest(ITERATIONS), DoThingRequest(ITERATIONS),
DoThingResponse, DoThingResponse,
deadline=kwarg_deadline, deadline=kwarg_deadline,
@ -123,7 +123,7 @@ async def test_service_call_lower_level_with_overrides():
) as channel: ) as channel:
stub = ExampleServiceStub(channel, deadline=deadline, metadata=metadata) stub = ExampleServiceStub(channel, deadline=deadline, metadata=metadata)
response = await stub._unary_unary( response = await stub._unary_unary(
"/service.ExampleService/DoThing", "/service.Test/DoThing",
DoThingRequest(ITERATIONS), DoThingRequest(ITERATIONS),
DoThingResponse, DoThingResponse,
timeout=kwarg_timeout, timeout=kwarg_timeout,

View File

@ -0,0 +1,10 @@
# 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",
}

View File

@ -5,8 +5,8 @@ from grpclib.client import Channel
class MockChannel(Channel): class MockChannel(Channel):
# noinspection PyMissingConstructor # noinspection PyMissingConstructor
def __init__(self, responses: List) -> None: def __init__(self, responses=None) -> None:
self.responses = responses self.responses = responses if responses else []
self.requests = [] self.requests = []
def request(self, route, cardinality, request, response_type, **kwargs): def request(self, route, cardinality, request, response_type, **kwargs):

View File

@ -2,10 +2,15 @@ import importlib
import json import json
import os import os
import sys import sys
import pytest
import betterproto
from betterproto.tests.util import get_directories, inputs_path
from collections import namedtuple from collections import namedtuple
from typing import Set
import pytest
import betterproto
from betterproto.tests.inputs import xfail
from betterproto.tests.mocks import MockChannel
from betterproto.tests.util import get_directories, get_test_case_json_data, inputs_path
# Force pure-python implementation instead of C++, otherwise imports # Force pure-python implementation instead of C++, otherwise imports
# break things because we can't properly reset the symbol database. # break things because we can't properly reset the symbol database.
@ -16,12 +21,34 @@ from google.protobuf.descriptor_pool import DescriptorPool
from google.protobuf.json_format import Parse from google.protobuf.json_format import Parse
excluded_test_cases = { class TestCases:
"googletypes_response", def __init__(self, path, services: Set[str], xfail: Set[str]):
"googletypes_response_embedded", _all = set(get_directories(path))
"service", _services = services
_messages = _all - services
_messages_with_json = {
test for test in _messages if get_test_case_json_data(test)
} }
test_case_names = {*get_directories(inputs_path)} - excluded_test_cases
self.all = self.apply_xfail_marks(_all, xfail)
self.services = self.apply_xfail_marks(_services, xfail)
self.messages = self.apply_xfail_marks(_messages, xfail)
self.messages_with_json = self.apply_xfail_marks(_messages_with_json, xfail)
@staticmethod
def apply_xfail_marks(test_set: Set[str], xfail: Set[str]):
return [
pytest.param(test, marks=pytest.mark.xfail) if test in xfail else test
for test in test_set
]
test_cases = TestCases(
path=inputs_path,
# test cases for services
services={"googletypes_response", "googletypes_response_embedded", "service"},
xfail=xfail.tests,
)
plugin_output_package = "betterproto.tests.output_betterproto" plugin_output_package = "betterproto.tests.output_betterproto"
reference_output_package = "betterproto.tests.output_reference" reference_output_package = "betterproto.tests.output_reference"
@ -30,7 +57,7 @@ reference_output_package = "betterproto.tests.output_reference"
TestData = namedtuple("TestData", "plugin_module, reference_module, json_data") TestData = namedtuple("TestData", "plugin_module, reference_module, json_data")
@pytest.fixture(scope="module", params=test_case_names) @pytest.fixture
def test_data(request): def test_data(request):
test_case_name = request.param test_case_name = request.param
@ -45,24 +72,28 @@ def test_data(request):
sys.path.append(reference_module_root) sys.path.append(reference_module_root)
yield TestData( yield (
TestData(
plugin_module=importlib.import_module( plugin_module=importlib.import_module(
f"{plugin_output_package}.{test_case_name}.{test_case_name}" f"{plugin_output_package}.{test_case_name}.{test_case_name}"
), ),
reference_module=importlib.import_module( reference_module=lambda: importlib.import_module(
f"{reference_output_package}.{test_case_name}.{test_case_name}_pb2" f"{reference_output_package}.{test_case_name}.{test_case_name}_pb2"
), ),
json_data=get_test_case_json_data(test_case_name), json_data=get_test_case_json_data(test_case_name),
) )
)
sys.path.remove(reference_module_root) sys.path.remove(reference_module_root)
@pytest.mark.parametrize("test_data", test_cases.messages, indirect=True)
def test_message_can_instantiated(test_data: TestData) -> None: def test_message_can_instantiated(test_data: TestData) -> None:
plugin_module, *_ = test_data plugin_module, *_ = test_data
plugin_module.Test() plugin_module.Test()
@pytest.mark.parametrize("test_data", test_cases.messages, indirect=True)
def test_message_equality(test_data: TestData) -> None: def test_message_equality(test_data: TestData) -> None:
plugin_module, *_ = test_data plugin_module, *_ = test_data
message1 = plugin_module.Test() message1 = plugin_module.Test()
@ -70,6 +101,7 @@ def test_message_equality(test_data: TestData) -> None:
assert message1 == message2 assert message1 == message2
@pytest.mark.parametrize("test_data", test_cases.messages_with_json, indirect=True)
def test_message_json(repeat, test_data: TestData) -> None: def test_message_json(repeat, test_data: TestData) -> None:
plugin_module, _, json_data = test_data plugin_module, _, json_data = test_data
@ -82,10 +114,17 @@ def test_message_json(repeat, test_data: TestData) -> None:
assert json.loads(json_data) == json.loads(message_json) assert json.loads(json_data) == json.loads(message_json)
@pytest.mark.parametrize("test_data", test_cases.services, indirect=True)
def test_service_can_be_instantiated(test_data: TestData) -> None:
plugin_module, _, json_data = test_data
plugin_module.TestStub(MockChannel())
@pytest.mark.parametrize("test_data", test_cases.messages_with_json, indirect=True)
def test_binary_compatibility(repeat, test_data: TestData) -> None: def test_binary_compatibility(repeat, test_data: TestData) -> None:
plugin_module, reference_module, json_data = test_data plugin_module, reference_module, json_data = test_data
reference_instance = Parse(json_data, reference_module.Test()) reference_instance = Parse(json_data, reference_module().Test())
reference_binary_output = reference_instance.SerializeToString() reference_binary_output = reference_instance.SerializeToString()
for _ in range(repeat): for _ in range(repeat):
@ -99,21 +138,10 @@ def test_binary_compatibility(repeat, test_data: TestData) -> None:
# # Generally this can't be relied on, but here we are aiming to match the # # Generally this can't be relied on, but here we are aiming to match the
# # existing Python implementation and aren't doing anything tricky. # # existing Python implementation and aren't doing anything tricky.
# # https://developers.google.com/protocol-buffers/docs/encoding#implications # # https://developers.google.com/protocol-buffers/docs/encoding#implications
assert bytes(plugin_instance_from_json) == reference_binary_output
assert bytes(plugin_instance_from_binary) == reference_binary_output
assert plugin_instance_from_json == plugin_instance_from_binary assert plugin_instance_from_json == plugin_instance_from_binary
assert ( assert (
plugin_instance_from_json.to_dict() == plugin_instance_from_binary.to_dict() plugin_instance_from_json.to_dict() == plugin_instance_from_binary.to_dict()
) )
"""
helper methods
"""
def get_test_case_json_data(test_case_name):
test_data_path = os.path.join(inputs_path, test_case_name, f"{test_case_name}.json")
if not os.path.exists(test_data_path):
return None
with open(test_data_path) as fh:
return fh.read()

View File

@ -48,3 +48,14 @@ def protoc_reference(path: str, output_dir: str):
f"protoc --python_out={output_dir} --proto_path={path} {path}/*.proto", f"protoc --python_out={output_dir} --proto_path={path} {path}/*.proto",
shell=True, shell=True,
) )
def get_test_case_json_data(test_case_name, json_file_name=None):
test_data_file_name = json_file_name if json_file_name else f"{test_case_name}.json"
test_data_file_path = os.path.join(inputs_path, test_case_name, test_data_file_name)
if not os.path.exists(test_data_file_path):
return None
with open(test_data_file_path) as fh:
return fh.read()