Skip to content

Commit 3cc7110

Browse files
committed
fix: address second round of review comments
- Add cycle detection in $ref resolution to prevent recursion blowups - Merge path-item and operation-level parameters for complete coverage - Report malformed JSONL output instead of silently swallowing parse errors
1 parent 0b09794 commit 3cc7110

1 file changed

Lines changed: 29 additions & 13 deletions

File tree

fuzz/mutate_fuzz.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -188,11 +188,13 @@ def mutate(spec: dict, n: int, rng: random.Random) -> list[str]:
188188

189189
def resolve_refs(spec: dict) -> dict:
190190
"""Recursively resolve all $ref pointers in the spec (in-place)."""
191-
def _resolve(node, root):
191+
def _resolve(node, root, seen_refs):
192192
if isinstance(node, dict):
193193
if "$ref" in node and isinstance(node["$ref"], str):
194194
ref = node["$ref"]
195195
if ref.startswith("#/"):
196+
if ref in seen_refs:
197+
return node
196198
parts = ref[2:].split("/")
197199
target = root
198200
for p in parts:
@@ -203,13 +205,13 @@ def _resolve(node, root):
203205
return node
204206
if isinstance(target, dict):
205207
resolved = copy.deepcopy(target)
206-
return _resolve(resolved, root)
208+
return _resolve(resolved, root, seen_refs | {ref})
207209
return node
208-
return {k: _resolve(v, root) for k, v in node.items()}
210+
return {k: _resolve(v, root, seen_refs) for k, v in node.items()}
209211
if isinstance(node, list):
210-
return [_resolve(item, root) for item in node]
212+
return [_resolve(item, root, seen_refs) for item in node]
211213
return node
212-
return _resolve(spec, spec)
214+
return _resolve(spec, spec, set())
213215

214216

215217
# Case generation -------------------------------------------------------------
@@ -283,6 +285,15 @@ def sample_value(schema: dict, rng: random.Random, depth: int = 0):
283285
return None
284286

285287

288+
def _merged_parameters(path_item: dict, op: dict) -> list[dict]:
289+
"""Merge path-item and operation-level parameters (op overrides)."""
290+
merged = {}
291+
for p in (path_item.get("parameters") or []) + (op.get("parameters") or []):
292+
if isinstance(p, dict):
293+
merged[(p.get("name"), p.get("in"))] = p
294+
return list(merged.values())
295+
296+
286297
def gen_cases(spec: dict, rng: random.Random, max_per_op: int = 2) -> list[dict]:
287298
"""Generate a small set of positive requests per op (oracle: must accept)."""
288299
cases = []
@@ -299,10 +310,12 @@ def gen_cases(spec: dict, rng: random.Random, max_per_op: int = 2) -> list[dict]
299310
break
300311
op_count += 1
301312

302-
# Concrete path: substitute each {token} with a small value matching
303-
# any declared path-parameter schema.
304-
path_params = {p["name"]: p for p in (op.get("parameters") or [])
305-
if isinstance(p, dict) and p.get("in") == "path"}
313+
params = _merged_parameters(item, op)
314+
path_params = {
315+
p["name"]: p
316+
for p in params
317+
if p.get("in") == "path" and isinstance(p.get("name"), str)
318+
}
306319
concrete = path
307320
for token in (s.split("}", 1)[0] for s in path.split("{")[1:]):
308321
pp = path_params.get(token, {})
@@ -319,8 +332,7 @@ def gen_cases(spec: dict, rng: random.Random, max_per_op: int = 2) -> list[dict]
319332
"headers": {},
320333
}
321334

322-
# Required query/header params: fill with conforming values
323-
for p in op.get("parameters") or []:
335+
for p in params:
324336
if not isinstance(p, dict):
325337
continue
326338
if not p.get("required"):
@@ -413,11 +425,15 @@ def run_validator(spec: dict, cases: list[dict], deps: str, lib: str,
413425
except subprocess.TimeoutExpired:
414426
return [{"phase": "timeout", "stderr": "validator subprocess timed out"}]
415427
out = []
428+
bad_lines = 0
416429
for line in r.stdout.splitlines():
417430
try:
418431
out.append(json.loads(line))
419-
except Exception:
420-
pass
432+
except json.JSONDecodeError:
433+
bad_lines += 1
434+
if bad_lines and not out:
435+
out.append({"phase": "subprocess_error", "rc": r.returncode,
436+
"stderr": f"malformed validator JSONL output ({bad_lines} lines)"})
421437
if r.returncode != 0 and not out:
422438
out.append({"phase": "subprocess_error", "rc": r.returncode,
423439
"stderr": r.stderr[-2000:]})

0 commit comments

Comments
 (0)