Detect entry-point of tests automatically

This commit is contained in:
boukeversteegh 2020-06-10 22:42:38 +02:00
parent 1a95a7988e
commit fb54917f2c
5 changed files with 61 additions and 36 deletions

View File

@ -312,7 +312,7 @@ $ pip install -e .
This project enforces [black](https://github.com/psf/black) python code formatting. This project enforces [black](https://github.com/psf/black) python code formatting.
Before commiting changes run: Before committing changes run:
```bash ```bash
pipenv run black . pipenv run black .
@ -333,7 +333,7 @@ Adding a standard test case is easy.
- Create a new directory `betterproto/tests/inputs/<name>` - Create a new directory `betterproto/tests/inputs/<name>`
- add `<name>.proto` with a message called `Test` - add `<name>.proto` with a message called `Test`
- add `<name>.json` with some test data - add `<name>.json` with some test data (optional)
It will be picked up automatically when you run the tests. It will be picked up automatically when you run the tests.

View File

@ -12,12 +12,12 @@ inputs/
## Test case directory structure ## Test case directory structure
Each testcase has a `<name>.proto` file with a message called `Test`, a matching `.json` file and optionally a custom test file called `test_*.py`. 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 ```bash
bool/ bool/
bool.proto bool.proto
bool.json bool.json # optional
test_bool.py # optional test_bool.py # optional
``` ```
@ -61,21 +61,22 @@ def test_value():
The following tests are automatically executed for all cases: The following tests are automatically executed for all cases:
- [x] Can the generated python code imported? - [x] Can the generated python code be imported?
- [x] Can the generated message class be instantiated? - [x] Can the generated message class be instantiated?
- [x] Is the generated code compatible with the Google's `grpc_tools.protoc` implementation? - [x] Is the generated code compatible with the Google's `grpc_tools.protoc` implementation?
- _when `.json` is present_
## Running the tests ## Running the tests
- `pipenv run generate` - `pipenv run generate`
This generates This generates:
- `betterproto/tests/output_betterproto` &mdash; *the plugin generated python classes* - `betterproto/tests/output_betterproto` &mdash; *the plugin generated python classes*
- `betterproto/tests/output_reference` &mdash; *reference implementation classes* - `betterproto/tests/output_reference` &mdash; *reference implementation classes*
- `pipenv run test` - `pipenv run test`
## Intentionally Failing tests ## 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. 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. When running `pytest`, they show up as `x` or `X` in the test results.

View File

@ -1,6 +1,6 @@
# Test cases that are expected to fail, e.g. unimplemented features or bug-fixes. # Test cases that are expected to fail, e.g. unimplemented features or bug-fixes.
# Remove from list when fixed. # Remove from list when fixed.
tests = { xfail = {
"import_circular_dependency", "import_circular_dependency",
"oneof_enum", # 63 "oneof_enum", # 63
"casing_message_field_uppercase", # 11 "casing_message_field_uppercase", # 11
@ -10,19 +10,6 @@ tests = {
"googletypes_value", # 9 "googletypes_value", # 9
} }
# Defines where the main package for this test resides.
# Needed to test relative package imports.
packages = {
"import_root_package_from_child": ".child",
"import_parent_package_from_child": ".parent.child",
"import_root_package_from_nested_child": ".nested.child",
"import_cousin_package": ".test.subpackage",
"import_cousin_package_same_name": ".test.subpackage",
"repeatedmessage": ".repeatedmessage",
"service": ".service",
}
services = { services = {
"googletypes_response", "googletypes_response",
"googletypes_response_embedded", "googletypes_response_embedded",

View File

@ -3,6 +3,7 @@ import json
import os import os
import sys import sys
from collections import namedtuple from collections import namedtuple
from types import ModuleType
from typing import Set from typing import Set
import pytest import pytest
@ -10,7 +11,12 @@ import pytest
import betterproto import betterproto
from betterproto.tests.inputs import config as test_input_config from betterproto.tests.inputs import config as test_input_config
from betterproto.tests.mocks import MockChannel from betterproto.tests.mocks import MockChannel
from betterproto.tests.util import get_directories, get_test_case_json_data, inputs_path from betterproto.tests.util import (
find_module,
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.
@ -50,16 +56,17 @@ class TestCases:
test_cases = TestCases( test_cases = TestCases(
path=inputs_path, path=inputs_path,
services=test_input_config.services, services=test_input_config.services,
xfail=test_input_config.tests, xfail=test_input_config.xfail,
) )
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"
TestData = namedtuple("TestData", ["plugin_module", "reference_module", "json_data"])
TestData = namedtuple(
"TestData", ["plugin_module", "reference_module", "json_data", "entry_point"] def module_has_entry_point(module: ModuleType):
) return any(hasattr(module, attr) for attr in ["Test", "TestStub"])
@pytest.fixture @pytest.fixture
@ -77,18 +84,23 @@ def test_data(request):
sys.path.append(reference_module_root) sys.path.append(reference_module_root)
test_package = test_case_name + test_input_config.packages.get(test_case_name, "") plugin_module = importlib.import_module(f"{plugin_output_package}.{test_case_name}")
plugin_module_entry_point = find_module(plugin_module, module_has_entry_point)
if not plugin_module_entry_point:
raise Exception(
f"Test case {repr(test_case_name)} has no entry point. "
+ "Please add a proto message or service called Test and recompile."
)
yield ( yield (
TestData( TestData(
plugin_module=importlib.import_module( plugin_module=plugin_module_entry_point,
f"{plugin_output_package}.{test_package}"
),
reference_module=lambda: 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),
entry_point=test_package,
) )
) )
@ -111,7 +123,7 @@ def test_message_equality(test_data: TestData) -> None:
@pytest.mark.parametrize("test_data", test_cases.messages_with_json, indirect=True) @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, entry_point = test_data plugin_module, _, json_data = test_data
for _ in range(repeat): for _ in range(repeat):
message: betterproto.Message = plugin_module.Test() message: betterproto.Message = plugin_module.Test()
@ -124,13 +136,13 @@ def test_message_json(repeat, test_data: TestData) -> None:
@pytest.mark.parametrize("test_data", test_cases.services, indirect=True) @pytest.mark.parametrize("test_data", test_cases.services, indirect=True)
def test_service_can_be_instantiated(test_data: TestData) -> None: def test_service_can_be_instantiated(test_data: TestData) -> None:
plugin_module, _, json_data, entry_point = test_data plugin_module, _, json_data = test_data
plugin_module.TestStub(MockChannel()) plugin_module.TestStub(MockChannel())
@pytest.mark.parametrize("test_data", test_cases.messages_with_json, indirect=True) @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, entry_point = 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()

View File

@ -1,6 +1,9 @@
import importlib
import os import os
import pathlib
import subprocess import subprocess
from typing import Generator from types import ModuleType
from typing import Callable, Generator, Optional
os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python" os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python"
@ -60,3 +63,25 @@ def get_test_case_json_data(test_case_name, json_file_name=None):
with open(test_data_file_path) as fh: with open(test_data_file_path) as fh:
return fh.read() return fh.read()
def find_module(
module: ModuleType, predicate: Callable[[ModuleType], bool]
) -> Optional[ModuleType]:
if predicate(module):
return module
module_path = pathlib.Path(*module.__path__)
for sub in list(module_path.glob("**/")):
if not sub.is_dir() or sub == module_path:
continue
sub_module_path = sub.relative_to(module_path)
sub_module_name = ".".join(sub_module_path.parts)
sub_module = importlib.import_module(f".{sub_module_name}", module.__name__)
if predicate(sub_module):
return sub_module
return None