Skip to content

Commit 9b4c074

Browse files
authored
Hypothesis testing (#136)
* testing with hypothesis * add failing case * 3.7 compat * 3.7 compat, take 2 * use hypothesis settings profiles * add failing hypothesis cycles * suggested branch_models_with_cycles * hypothesis generating cyclic references * fix recursive hypothesis tests * check errors
1 parent 9899af5 commit 9b4c074

File tree

4 files changed

+122
-1
lines changed

4 files changed

+122
-1
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ docs/_build/
2727
tests/package.json
2828
tests/package-lock.json
2929
tests/node_modules
30+
/.hypothesis/

tests/conftest.py

+6
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,17 @@
77
from typing import Any
88

99
import pytest
10+
from hypothesis import settings
1011

1112
from pydantic_core import SchemaValidator
1213

1314
__all__ = ('Err',)
1415

16+
hyp_max_examples = os.getenv('HYPOTHESIS_MAX_EXAMPLES')
17+
if hyp_max_examples:
18+
settings.register_profile('custom', max_examples=int(hyp_max_examples))
19+
settings.load_profile('custom')
20+
1521

1622
@dataclass
1723
class Err:

tests/requirements.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
coverage==6.4.1
22
dirty-equals==0.4
3+
hypothesis==6.48.1
34
pytest==7.1.2
45
pytest-benchmark==3.4.1
5-
pytest-mock==3.7.0
6+
pytest-mock==3.8.1
67
pytest-sugar==0.9.4
78
pytest-timeout==2.1.0
89
pydantic==1.9.1;python_version>="3.8"

tests/test_hypothesis.py

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
from datetime import datetime, timezone
2+
from typing import Optional
3+
4+
import pytest
5+
from dirty_equals import AnyThing, IsBytes, IsList, IsStr
6+
from hypothesis import given, strategies
7+
from typing_extensions import TypedDict
8+
9+
from pydantic_core import SchemaValidator, ValidationError
10+
11+
12+
@pytest.fixture(scope='module')
13+
def datetime_schema():
14+
return SchemaValidator({'type': 'datetime'})
15+
16+
17+
@given(strategies.datetimes())
18+
def test_datetime_datetime(datetime_schema, data):
19+
assert datetime_schema.validate_python(data) == data
20+
21+
22+
@given(strategies.integers(min_value=-11_676_096_000, max_value=253_402_300_799_000))
23+
def test_datetime_int(datetime_schema, data):
24+
if abs(data) > 20_000_000_000:
25+
microsecond = (data % 1000) * 1000
26+
expected = datetime.fromtimestamp(data // 1000, tz=timezone.utc).replace(tzinfo=None, microsecond=microsecond)
27+
else:
28+
expected = datetime.fromtimestamp(data, tz=timezone.utc).replace(tzinfo=None)
29+
30+
assert datetime_schema.validate_python(data) == expected, data
31+
32+
33+
@given(strategies.binary())
34+
def test_datetime_binary(datetime_schema, data):
35+
try:
36+
datetime_schema.validate_python(data)
37+
except ValidationError as exc:
38+
assert exc.errors() == [
39+
{
40+
'kind': 'datetime_parsing',
41+
'loc': [],
42+
'message': IsStr(regex='Value must be a valid datetime, .+'),
43+
'input_value': IsBytes(),
44+
'context': {'error': IsStr()},
45+
}
46+
]
47+
48+
49+
@pytest.fixture(scope='module')
50+
def recursive_schema():
51+
return SchemaValidator(
52+
{
53+
'type': 'typed-dict',
54+
'ref': 'Branch',
55+
'fields': {
56+
'name': {'schema': {'type': 'str'}},
57+
'sub_branch': {
58+
'schema': {'type': 'nullable', 'schema': {'type': 'recursive-ref', 'schema_ref': 'Branch'}},
59+
'default': None,
60+
},
61+
},
62+
}
63+
)
64+
65+
66+
def test_recursive_simple(recursive_schema):
67+
assert recursive_schema.validate_python({'name': 'root'}) == {'name': 'root', 'sub_branch': None}
68+
69+
70+
class BranchModel(TypedDict):
71+
name: str
72+
sub_branch: Optional['BranchModel']
73+
74+
75+
@given(strategies.from_type(BranchModel))
76+
def test_recursive(recursive_schema, data):
77+
assert recursive_schema.validate_python(data) == data
78+
79+
80+
@strategies.composite
81+
def branch_models_with_cycles(draw, existing=None):
82+
if existing is None:
83+
existing = []
84+
model = BranchModel(name=draw(strategies.text()), sub_branch=None)
85+
existing.append(model)
86+
model['sub_branch'] = draw(
87+
strategies.none()
88+
| strategies.builds(BranchModel, name=strategies.text(), sub_branch=branch_models_with_cycles(existing))
89+
| strategies.sampled_from(existing)
90+
)
91+
return model
92+
93+
94+
@given(branch_models_with_cycles())
95+
def test_recursive_cycles(recursive_schema, data):
96+
try:
97+
assert recursive_schema.validate_python(data) == data
98+
except ValidationError as exc:
99+
assert exc.errors() == [
100+
{
101+
'kind': 'recursion_loop',
102+
'loc': IsList(length=(1, None)),
103+
'message': 'Recursion error - cyclic reference detected',
104+
'input_value': AnyThing(),
105+
}
106+
]
107+
108+
109+
def test_recursive_broken(recursive_schema):
110+
data = {'name': 'x'}
111+
data['sub_branch'] = data
112+
with pytest.raises(ValidationError, match='Recursion error - cyclic reference detected'):
113+
recursive_schema.validate_python(data)

0 commit comments

Comments
 (0)