import copy
import json
import os
import random
import re
SEED = int(os.environ.get("JSONCOMPAT_SCHEMA_SEED", "3503345870"))
PRIMITIVES = [None, True, False, -3, -1, 0, 1, 2, 4, 0.5, "", "x", "é猫", [], {}]
def equal(a, b):
if isinstance(a, bool) or isinstance(b, bool):
return type(a) is type(b) and a == b
if isinstance(a, (int, float)) and isinstance(b, (int, float)):
return a == b
if type(a) is not type(b):
return False
if isinstance(a, list):
return len(a) == len(b) and all(equal(x, y) for x, y in zip(a, b))
if isinstance(a, dict):
return a.keys() == b.keys() and all(equal(v, b[k]) for k, v in a.items())
return a == b
def matches(schema, value, root=None):
if isinstance(schema, bool):
return schema
root = schema if root is None else root
if "$ref" in schema:
target = root
for token in schema["$ref"][2:].split("/"):
target = target[token.replace("~1", "/").replace("~0", "~")]
if not matches(target, value, root):
return False
types = dict(null=value is None, boolean=type(value) is bool,
integer=type(value) in (int, float) and value % 1 == 0,
number=type(value) in (int, float), string=isinstance(value, str),
array=isinstance(value, list), object=isinstance(value, dict))
if "type" in schema:
kinds = schema["type"] if isinstance(schema["type"], list) else [schema["type"]]
if not any(types[k] for k in kinds):
return False
if "const" in schema and not equal(value, schema["const"]):
return False
if "enum" in schema and not any(equal(value, x) for x in schema["enum"]):
return False
for key, check in (("allOf", all), ("anyOf", any), ("oneOf", lambda flags: sum(flags) == 1)):
if key in schema and not check(matches(s, value, root) for s in schema[key]):
return False
if "not" in schema and matches(schema["not"], value, root):
return False
if "if" in schema:
branch = "then" if matches(schema["if"], value, root) else "else"
if not matches(schema.get(branch, True), value, root):
return False
if types["number"]:
if value < schema.get("minimum", value) or value > schema.get("maximum", value):
return False
if "multipleOf" in schema and value % schema["multipleOf"] != 0:
return False
if types["string"]:
if len(value) < schema.get("minLength", 0) or len(value) > schema.get("maxLength", len(value)):
return False
if "pattern" in schema and re.search(schema["pattern"], value) is None:
return False
if types["array"]:
if len(value) < schema.get("minItems", 0) or len(value) > schema.get("maxItems", len(value)):
return False
prefix = schema.get("prefixItems", [])
if not all(matches(prefix[i] if i < len(prefix) else schema.get("items", True), item, root) for i, item in enumerate(value)):
return False
if "contains" in schema:
count = sum(matches(schema["contains"], item, root) for item in value)
if count < schema.get("minContains", 1) or count > schema.get("maxContains", count):
return False
if schema.get("uniqueItems") and any(equal(value[i], value[j]) for i in range(len(value)) for j in range(i)):
return False
if types["object"]:
if not set(schema.get("required", [])).issubset(value):
return False
props = schema.get("properties", {})
if not all(matches(props.get(k, schema.get("additionalProperties", True)), v, root) for k, v in value.items()):
return False
for k, required in schema.get("dependentRequired", {}).items():
if k in value and not set(required).issubset(value):
return False
return True
def schema(rng, depth):
leaves = [True, False, {"type": "integer", "minimum": rng.randrange(-3, 3)},
{"type": "number", "multipleOf": rng.choice([1, 2, 0.5])},
{"type": "string", "minLength": rng.randrange(3), "pattern": rng.choice(["x", "^", "é"])},
{"enum": rng.sample(PRIMITIVES[:13], 3)}, {"const": rng.choice(PRIMITIVES)}]
if depth == 0 or rng.randrange(4) == 0:
return copy.deepcopy(rng.choice(leaves))
op = rng.randrange(8)
if op < 3:
return {("allOf", "anyOf", "oneOf")[op]: [schema(rng, depth - 1) for _ in range(2)]}
if op == 3:
return {"not": schema(rng, depth - 1)}
if op == 4:
return {"if": schema(rng, depth - 1), "then": schema(rng, depth - 1), "else": schema(rng, depth - 1)}
if op == 5:
return {"type": "array", "prefixItems": [schema(rng, depth - 1)], "items": schema(rng, depth - 1), "uniqueItems": rng.choice([True, False])}
if op == 6:
return {"type": "array", "contains": schema(rng, depth - 1), "minContains": rng.randrange(3), "maxContains": 3}
return {"type": "object", "properties": {"x": schema(rng, depth - 1), "a/b~": schema(rng, depth - 1)},
"required": rng.choice([[], ["x"], ["x", "a/b~"]]), "additionalProperties": rng.choice([True, False]),
"dependentRequired": {"x": ["a/b~"]} if rng.randrange(2) else {}}
def value(rng, depth):
if depth == 0 or rng.randrange(3) == 0:
return copy.deepcopy(rng.choice(PRIMITIVES))
if rng.randrange(2):
return [value(rng, depth - 1) for _ in range(rng.randrange(4))]
return {k: value(rng, depth - 1) for k in rng.sample(["x", "a/b~", "extra"], rng.randrange(4))}
def cases():
rng = random.Random(SEED)
for index in range(int(os.environ.get("JSONCOMPAT_SCHEMA_CASES", "128"))):
base = schema(rng, 3)
values = PRIMITIVES + [value(rng, 3) for _ in range(80)]
valid = [v for v in values if matches(base, v)]
invalid = [v for v in values if not matches(base, v)]
variants = [base, {"not": {"not": base}}, {"allOf": [True, base]},
{"$defs": {"a/b~": base}, "$ref": "#/$defs/a~1b~0"}]
for variant, s in enumerate(variants):
yield dict(name=f"generated_{SEED}_{index}_{variant}", schema=s, valid=valid, invalid=invalid,
inline=None, wires=[], stress=True, independent=True, seed=SEED)
def reductions(value):
if isinstance(value, dict):
for key in value:
yield {k: v for k, v in value.items() if k != key}
for key, child in value.items():
for reduced in reductions(child):
yield dict(value, **{key: reduced})
elif isinstance(value, list):
for i in range(len(value)):
yield value[:i] + value[i+1:]
for i, child in enumerate(value):
for reduced in reductions(child):
yield value[:i] + [reduced] + value[i+1:]
elif isinstance(value, str) and value:
yield ""
if len(value) > 1:
yield value[:1]
elif type(value) in (int, float) and value != 0:
yield 0
if __name__ == "__main__":
print(json.dumps(list(cases()), ensure_ascii=True))
def minimize_failure(case, directory):
import subprocess
from python_dataclasses_differential import load, accepted, wire
import sys
cli = os.environ["JSONCOMPAT_TEST_CLI"]
source = directory / "minimized.py"
schema_file = directory / "minimized.schema.json"
probes = 0
def disagreement(s, values):
nonlocal probes
probes += 1
schema_file.write_text(json.dumps(s))
build = subprocess.run([cli, "codegen", "--target", "dataclasses", str(schema_file)], capture_output=True, text=True)
if build.returncode:
return None
source.write_text(build.stdout)
for mode in ("optimized", "general"):
module, _ = load(source, mode)
try:
model = module.JSONCOMPAT_MODEL
for v in values:
expected = matches(s, v)
for op, run in (("from_value", lambda: model.from_value(v)),
("deserialize", lambda: model.deserialize(wire(v)))):
ok, result = accepted(run)
if ok != expected:
return v, mode, op
if ok and not equal(result.to_value(), v):
return v, mode, "round_trip_value"
ok, instance = accepted(lambda: model.from_value(v, skip_validation=True))
if ok:
for op, run in (("serialize", instance.serialize), ("to_value", instance.to_value)):
if accepted(run)[0] != expected:
return v, mode, op
finally:
sys.modules.pop(module.__name__, None)
return None
s = case["schema"]
found = disagreement(s, case["valid"] + case["invalid"])
if found is None:
return
v, mode, op = found
while probes < 160:
changed = False
for smaller in reductions(v):
found = disagreement(s, [smaller])
if found:
v, mode, op = found
changed = True
break
if probes >= 160:
break
if changed:
continue
for smaller in reductions(s):
try:
found = disagreement(smaller, [v])
except (KeyError, TypeError, ValueError):
found = None if found:
s = smaller
v, mode, op = found
changed = True
break
if probes >= 160:
break
if not changed:
break
schema_file.write_text(json.dumps(s, indent=2))
(directory / "minimized.failure.json").write_text(json.dumps(dict(seed=case["seed"], name=case["name"],
schema=s, value=v, mode=mode, operation=op, expected=matches(s, v), probes=probes), indent=2))