Detect entry-point of tests automatically
This commit is contained in:
parent
1a95a7988e
commit
fb54917f2c
@ -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.
|
||||
|
||||
|
@ -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
|
||||
This generates:
|
||||
- `betterproto/tests/output_betterproto` — *the plugin generated python classes*
|
||||
- `betterproto/tests/output_reference` — *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.
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user