Skip to content

Commit f5b6331

Browse files
ben-ednaspoorcc
authored andcommitted
Use hardcoded schema
1 parent 37ab97b commit f5b6331

1 file changed

Lines changed: 90 additions & 249 deletions

File tree

tests/test_fuzzing.py

Lines changed: 90 additions & 249 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,14 @@
33
from __future__ import annotations
44

55
import os
6-
import re
76
import tempfile
87
from contextlib import suppress
9-
from typing import Any, Dict, Mapping, Tuple
8+
from typing import Any
109

1110
import yaml
1211
from hypothesis import given, settings
1312
from hypothesis import strategies as st
14-
15-
# StrictYAML is a runtime dependency:
16-
# pip install strictyaml hypothesis
17-
from strictyaml import Any as SyAny
18-
from strictyaml import (
19-
Bool,
20-
EmptyList,
21-
EmptyNone,
22-
Enum,
23-
Float,
24-
Int,
25-
Map,
26-
MapPattern,
27-
Regex,
28-
Seq,
29-
Str,
30-
as_document,
31-
load,
32-
)
13+
from strictyaml import as_document, load
3314

3415
from dfetch.__main__ import DfetchFatalException, run
3516
from dfetch.manifest.manifest import Manifest
@@ -57,231 +38,94 @@
5738
else:
5839
settings.load_profile("dev")
5940

41+
# Avoid control chars and NUL to prevent OS/path/subprocess issues in tests
42+
SAFE_TEXT = st.text(
43+
alphabet=st.characters(
44+
min_codepoint=32, blacklist_categories=("Cs",)
45+
), # no controls/surrogates
46+
min_size=0,
47+
max_size=64,
48+
)
6049

61-
def _classname(obj: Any) -> str:
62-
return obj.__class__.__name__
50+
# NUMBER = Int() | Float() with finite floats
51+
SAFE_NUMBER = st.one_of(
52+
st.integers(),
53+
st.floats(allow_nan=False, allow_infinity=False),
54+
)
6355

6456

65-
def _get_map_items(m: Map) -> Mapping[Any, Any]:
66-
"""
67-
StrictYAML's Map stores the key->validator mapping internally.
68-
It has varied attribute names across versions; try common ones.
69-
"""
70-
for attr in ("_validator", "_map", "map", "mapping"):
71-
val = getattr(m, attr, None)
72-
if isinstance(val, Mapping):
73-
return val
74-
raise TypeError("Unsupported StrictYAML Map internals; cannot find mapping dict.")
57+
def opt_str():
58+
"""Small helper for optional text fields."""
59+
return st.none() | SAFE_TEXT
60+
61+
62+
remote_entry = st.builds(
63+
lambda name, url_base, default: {
64+
k: v
65+
for k, v in {
66+
"name": name,
67+
"url-base": url_base,
68+
"default": default,
69+
}.items()
70+
if v is not None
71+
},
72+
name=SAFE_TEXT.filter(lambda s: len(s) > 0),
73+
url_base=SAFE_TEXT.filter(lambda s: len(s) > 0),
74+
default=st.none() | st.booleans(),
75+
)
7576

77+
vcs_enum = st.sampled_from(["git", "svn"])
78+
79+
ignore_list = st.lists(SAFE_TEXT, min_size=1, max_size=5)
80+
81+
project_entry = st.builds(
82+
lambda name, dst, branch, tag, revision, url, repo_path, remote, patch, vcs, src, ignore: {
83+
k: v
84+
for k, v in {
85+
"name": name,
86+
"dst": dst,
87+
"branch": branch,
88+
"tag": tag,
89+
"revision": revision,
90+
"url": url,
91+
"repo-path": repo_path,
92+
"remote": remote,
93+
"patch": patch,
94+
"vcs": vcs,
95+
"src": src,
96+
"ignore": ignore,
97+
}.items()
98+
if v is not None
99+
},
100+
name=SAFE_TEXT.filter(lambda s: len(s) > 0),
101+
dst=opt_str(),
102+
branch=opt_str(),
103+
tag=opt_str(),
104+
revision=opt_str(),
105+
url=opt_str(),
106+
repo_path=opt_str(),
107+
remote=opt_str(),
108+
patch=opt_str(),
109+
vcs=st.none() | vcs_enum,
110+
src=opt_str(),
111+
ignore=st.one_of(ignore_list, st.just([])),
112+
)
76113

77-
def _unwrap_optional_key(k: Any) -> Tuple[str, bool]:
78-
"""
79-
Returns (key_name, is_optional).
80-
Optional('b', default=...) is used *as a key* inside Map({...}).
81-
"""
82-
if _classname(k) == "Optional":
83-
for attr in ("_key", "key"):
84-
name = getattr(k, attr, None)
85-
if isinstance(name, str):
86-
return name, True
87-
return str(k), True
88-
if isinstance(k, str):
89-
return k, False
90-
return str(k), False
91-
92-
93-
def _enum_values(e: Enum) -> Any:
94-
vals = getattr(e, "_restricted_to", None)
95-
if vals:
96-
return list(vals)
97-
raise TypeError("Unsupported StrictYAML Enum internals; cannot read choices.")
98-
99-
100-
def _regex_pattern(r: Regex) -> re.Pattern:
101-
for attr in ("_regex", "regex", "pattern"):
102-
pat = getattr(r, attr, None)
103-
if isinstance(pat, (str, re.Pattern)):
104-
return re.compile(pat) if isinstance(pat, str) else pat
105-
raise TypeError("Unsupported StrictYAML Regex internals; cannot read pattern.")
106-
107-
108-
def _mappattern_parts(mp: MapPattern) -> Tuple[Any, Any, int | None, int | None]:
109-
key_v = None
110-
val_v = None
111-
min_k = getattr(mp, "minimum_keys", None)
112-
max_k = getattr(mp, "maximum_keys", None)
113-
for attr in ("_key_validator", "key_validator"):
114-
key_v = getattr(mp, attr, None) or key_v
115-
for attr in ("_value_validator", "value_validator"):
116-
val_v = getattr(mp, attr, None) or val_v
117-
if key_v is None or val_v is None:
118-
raise TypeError("Unsupported StrictYAML MapPattern internals.")
119-
return key_v, val_v, min_k, max_k
120-
121-
122-
def strictyaml_to_strategy(
123-
validator: Any, *, default_text_alphabet=st.characters(), default_max_list=5
124-
):
125-
"""
126-
Convert a StrictYAML validator into a Hypothesis strategy that yields
127-
*Python data structures* which conform to the schema.
128-
"""
129-
name = _classname(validator)
130-
131-
if isinstance(validator, Str):
132-
return st.text(alphabet=default_text_alphabet)
133-
134-
if isinstance(validator, Int):
135-
return st.integers()
136-
137-
if isinstance(validator, Float):
138-
return st.floats(allow_nan=False, allow_infinity=False)
139-
140-
if isinstance(validator, Bool):
141-
return st.booleans()
142-
143-
if isinstance(validator, Enum):
144-
values = _enum_values(validator)
145-
return st.sampled_from(values)
146-
147-
if isinstance(validator, Regex):
148-
pattern = _regex_pattern(validator)
149-
return st.from_regex(pattern, fullmatch=True)
150-
151-
if isinstance(validator, Seq):
152-
item_v = None
153-
for attr in ("_validator", "validator", "_item_validator", "item_validator"):
154-
item_v = getattr(validator, attr, None) or item_v
155-
if item_v is None:
156-
raise TypeError(
157-
"Unsupported StrictYAML Seq internals; cannot find item validator."
158-
)
159-
return st.lists(
160-
strictyaml_to_strategy(
161-
item_v,
162-
default_text_alphabet=default_text_alphabet,
163-
default_max_list=default_max_list,
164-
),
165-
min_size=1,
166-
max_size=default_max_list,
167-
)
168-
169-
if isinstance(validator, EmptyList):
170-
return st.just([])
171-
172-
if isinstance(validator, Map):
173-
items = _get_map_items(validator)
174-
required: Dict[str, Any] = {}
175-
optional: Dict[str, Any] = {}
176-
177-
for raw_key, val_validator in items.items():
178-
key_name, is_opt = _unwrap_optional_key(raw_key)
179-
if is_opt:
180-
optional[key_name] = strictyaml_to_strategy(
181-
val_validator,
182-
default_text_alphabet=default_text_alphabet,
183-
default_max_list=default_max_list,
184-
)
185-
else:
186-
required[key_name] = strictyaml_to_strategy(
187-
val_validator,
188-
default_text_alphabet=default_text_alphabet,
189-
default_max_list=default_max_list,
190-
)
191-
192-
base = st.fixed_dictionaries(required)
193-
194-
def with_optional(base_dict: Dict[str, Any]):
195-
if not optional:
196-
return st.just(base_dict)
197-
opt_kv_strats = [st.tuples(st.just(k), s) for k, s in optional.items()]
198-
199-
chosen = st.lists(st.one_of(*opt_kv_strats), unique_by=lambda kv: kv[0])
200-
return chosen.map(lambda kvs: {**base_dict, **dict(kvs)})
201-
202-
return base.flatmap(with_optional)
203-
204-
if isinstance(validator, MapPattern):
205-
key_v, val_v, min_k, max_k = _mappattern_parts(validator)
206-
key_strat = strictyaml_to_strategy(
207-
key_v,
208-
default_text_alphabet=default_text_alphabet,
209-
default_max_list=default_max_list,
210-
)
211-
val_strat = strictyaml_to_strategy(
212-
val_v,
213-
default_text_alphabet=default_text_alphabet,
214-
default_max_list=default_max_list,
215-
)
216-
217-
return st.dictionaries(
218-
keys=key_strat,
219-
values=val_strat,
220-
min_size=min_k or 0,
221-
max_size=max_k or default_max_list,
222-
)
223-
224-
if _classname(validator) in ("OrValidator", "Or"):
225-
children = None
226-
227-
for attr in ("validators", "_validators", "choices", "_choices"):
228-
vs = getattr(validator, attr, None)
229-
if isinstance(vs, (list, tuple)) and len(vs) > 0:
230-
children = list(vs)
231-
break
232-
233-
if children is None:
234-
left = None
235-
right = None
236-
for la in ("_a", "a", "_left", "left", "_lhs", "lhs", "_validator_a"):
237-
if getattr(validator, la, None) is not None:
238-
left = getattr(validator, la)
239-
break
240-
for ra in ("_b", "b", "_right", "right", "_rhs", "rhs", "_validator_b"):
241-
if getattr(validator, ra, None) is not None:
242-
right = getattr(validator, ra)
243-
break
244-
if left is not None and right is not None:
245-
children = [left, right]
246-
247-
if not children:
248-
raise TypeError(
249-
"Unsupported StrictYAML OrValidator internals; no children found."
250-
)
251-
252-
branch_strats = [
253-
strictyaml_to_strategy(
254-
c,
255-
default_text_alphabet=default_text_alphabet,
256-
default_max_list=default_max_list,
257-
)
258-
for c in children
259-
]
260-
return st.one_of(branch_strats)
261-
262-
if isinstance(validator, SyAny):
263-
leaf = st.one_of(
264-
st.booleans(),
265-
st.integers(),
266-
st.floats(allow_nan=False, allow_infinity=False),
267-
st.text(),
268-
)
269-
return st.recursive(
270-
leaf,
271-
lambda inner: st.one_of(
272-
st.lists(inner, max_size=3),
273-
st.dictionaries(st.text(), inner, max_size=3),
274-
),
275-
max_leaves=10,
276-
)
277-
278-
if isinstance(validator, EmptyNone):
279-
return st.none()
280-
281-
# If we reach here, add more mappings (e.g., Decimal, Datetime, Email, etc.) as needed.
282-
raise NotImplementedError(
283-
f"No strategy mapping implemented for StrictYAML validator: {name}"
284-
)
114+
remotes_seq = st.none() | st.lists(remote_entry, min_size=1, max_size=4)
115+
projects_seq = st.lists(project_entry, min_size=1, max_size=6)
116+
117+
manifest_strategy = st.builds(
118+
lambda version, remotes, projects: {
119+
"manifest": {
120+
"version": version,
121+
**({"remotes": remotes} if remotes is not None else {}),
122+
"projects": projects,
123+
}
124+
},
125+
version=SAFE_NUMBER,
126+
remotes=remotes_seq,
127+
projects=projects_seq,
128+
)
285129

286130

287131
def validate_with_strictyaml(data: Any, yaml_schema: Any) -> None:
@@ -292,17 +136,14 @@ def validate_with_strictyaml(data: Any, yaml_schema: Any) -> None:
292136
as_document(data, yaml_schema) # will raise YAMLSerializationError on mismatch
293137

294138

295-
data_strategy = strictyaml_to_strategy(schema)
296-
297-
298-
@given(data_strategy)
139+
@given(manifest_strategy)
299140
def test_data_conforms_to_schema(data):
300141
"""Validate by attempting to serialize via StrictYAML."""
301142
# If data violates the schema, this raises and Hypothesis will shrink to a minimal counterexample.
302143
validate_with_strictyaml(data, schema)
303144

304145

305-
@given(data_strategy)
146+
@given(manifest_strategy)
306147
def test_manifest_can_be_created(data):
307148
"""Validate by attempting to construct a Manifest."""
308149
try:
@@ -311,7 +152,7 @@ def test_manifest_can_be_created(data):
311152
pass
312153

313154

314-
@given(data_strategy)
155+
@given(manifest_strategy)
315156
def test_check(data):
316157
"""Validate check comand."""
317158
with suppress(DfetchFatalException):
@@ -322,7 +163,7 @@ def test_check(data):
322163
run(["check"])
323164

324165

325-
@given(data_strategy)
166+
@given(manifest_strategy)
326167
def test_update(data):
327168
"""Validate update comand."""
328169
with suppress(DfetchFatalException):
@@ -337,7 +178,7 @@ def test_update(data):
337178

338179
settings.load_profile("manual")
339180

340-
example = data_strategy.example()
181+
example = manifest_strategy.example()
341182
print("One generated example:\n", example)
342183

343184
# Show the YAML StrictYAML would emit for the example:

0 commit comments

Comments
 (0)