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.
Before commiting changes run:
Before committing changes run:
```bash
pipenv run black .
@ -333,7 +333,7 @@ Adding a standard test case is easy.
- Create a new directory `betterproto/tests/inputs/<name>`
- 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.

View File

@ -12,12 +12,12 @@ inputs/
## 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
bool/
bool.proto
bool.json
bool.json # optional
test_bool.py # optional
```
@ -61,21 +61,22 @@ def test_value():
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] 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
- `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 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.

View File

@ -1,6 +1,6 @@
# Test cases that are expected to fail, e.g. unimplemented features or bug-fixes.
# Remove from list when fixed.
tests = {
xfail = {
"import_circular_dependency",
"oneof_enum", # 63
"casing_message_field_uppercase", # 11
@ -10,19 +10,6 @@ tests = {
"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 = {
"googletypes_response",
"googletypes_response_embedded",

View File

@ -3,6 +3,7 @@ import json
import os
import sys
from collections import namedtuple
from types import ModuleType
from typing import Set
import pytest
@ -10,7 +11,12 @@ import pytest
import betterproto
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
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
# break things because we can't properly reset the symbol database.
@ -50,16 +56,17 @@ class TestCases:
test_cases = TestCases(
path=inputs_path,
services=test_input_config.services,
xfail=test_input_config.tests,
xfail=test_input_config.xfail,
)
plugin_output_package = "betterproto.tests.output_betterproto"
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
@ -77,18 +84,23 @@ def test_data(request):
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 (
TestData(
plugin_module=importlib.import_module(
f"{plugin_output_package}.{test_package}"
),
plugin_module=plugin_module_entry_point,
reference_module=lambda: importlib.import_module(
f"{reference_output_package}.{test_case_name}.{test_case_name}_pb2"
),
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)
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):
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)
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())
@pytest.mark.parametrize("test_data", test_cases.messages_with_json, indirect=True)
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_binary_output = reference_instance.SerializeToString()

View File

@ -1,6 +1,9 @@
import importlib
import os
import pathlib
import subprocess
from typing import Generator
from types import ModuleType
from typing import Callable, Generator, Optional
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:
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