123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- """
- Python representations of the JSON Schema Test Suite tests.
- """
- from __future__ import annotations
- from contextlib import suppress
- from functools import partial
- from pathlib import Path
- from typing import TYPE_CHECKING, Any
- import json
- import os
- import re
- import subprocess
- import sys
- import unittest
- from attrs import field, frozen
- from referencing import Registry
- import referencing.jsonschema
- if TYPE_CHECKING:
- from collections.abc import Iterable, Mapping, Sequence
- import pyperf
- from jsonschema.validators import _VALIDATORS
- import jsonschema
- _DELIMITERS = re.compile(r"[\W\- ]+")
- def _find_suite():
- root = os.environ.get("JSON_SCHEMA_TEST_SUITE")
- if root is not None:
- return Path(root)
- root = Path(jsonschema.__file__).parent.parent / "json"
- if not root.is_dir(): # pragma: no cover
- raise ValueError(
- (
- "Can't find the JSON-Schema-Test-Suite directory. "
- "Set the 'JSON_SCHEMA_TEST_SUITE' environment "
- "variable or run the tests from alongside a checkout "
- "of the suite."
- ),
- )
- return root
- @frozen
- class Suite:
- _root: Path = field(factory=_find_suite)
- _remotes: referencing.jsonschema.SchemaRegistry = field(init=False)
- def __attrs_post_init__(self):
- jsonschema_suite = self._root.joinpath("bin", "jsonschema_suite")
- argv = [sys.executable, str(jsonschema_suite), "remotes"]
- remotes = subprocess.check_output(argv).decode("utf-8")
- resources = json.loads(remotes)
- li = "http://localhost:1234/locationIndependentIdentifierPre2019.json"
- li4 = "http://localhost:1234/locationIndependentIdentifierDraft4.json"
- registry = Registry().with_resources(
- [
- (
- li,
- referencing.jsonschema.DRAFT7.create_resource(
- contents=resources.pop(li),
- ),
- ),
- (
- li4,
- referencing.jsonschema.DRAFT4.create_resource(
- contents=resources.pop(li4),
- ),
- ),
- ],
- ).with_contents(
- resources.items(),
- default_specification=referencing.jsonschema.DRAFT202012,
- )
- object.__setattr__(self, "_remotes", registry)
- def benchmark(self, runner: pyperf.Runner): # pragma: no cover
- for name, Validator in _VALIDATORS.items():
- self.version(name=name).benchmark(
- runner=runner,
- Validator=Validator,
- )
- def version(self, name) -> Version:
- return Version(
- name=name,
- path=self._root / "tests" / name,
- remotes=self._remotes,
- )
- @frozen
- class Version:
- _path: Path
- _remotes: referencing.jsonschema.SchemaRegistry
- name: str
- def benchmark(self, **kwargs): # pragma: no cover
- for case in self.cases():
- case.benchmark(**kwargs)
- def cases(self) -> Iterable[_Case]:
- return self._cases_in(paths=self._path.glob("*.json"))
- def format_cases(self) -> Iterable[_Case]:
- return self._cases_in(paths=self._path.glob("optional/format/*.json"))
- def optional_cases_of(self, name: str) -> Iterable[_Case]:
- return self._cases_in(paths=[self._path / "optional" / f"{name}.json"])
- def to_unittest_testcase(self, *groups, **kwargs):
- name = kwargs.pop("name", "Test" + self.name.title().replace("-", ""))
- methods = {
- method.__name__: method
- for method in (
- test.to_unittest_method(**kwargs)
- for group in groups
- for case in group
- for test in case.tests
- )
- }
- cls = type(name, (unittest.TestCase,), methods)
- # We're doing crazy things, so if they go wrong, like a function
- # behaving differently on some other interpreter, just make them
- # not happen.
- with suppress(Exception):
- cls.__module__ = _someone_save_us_the_module_of_the_caller()
- return cls
- def _cases_in(self, paths: Iterable[Path]) -> Iterable[_Case]:
- for path in paths:
- for case in json.loads(path.read_text(encoding="utf-8")):
- yield _Case.from_dict(
- case,
- version=self,
- subject=path.stem,
- remotes=self._remotes,
- )
- @frozen
- class _Case:
- version: Version
- subject: str
- description: str
- schema: Mapping[str, Any] | bool
- tests: list[_Test]
- comment: str | None = None
- specification: Sequence[dict[str, str]] = ()
- @classmethod
- def from_dict(cls, data, remotes, **kwargs):
- data.update(kwargs)
- tests = [
- _Test(
- version=data["version"],
- subject=data["subject"],
- case_description=data["description"],
- schema=data["schema"],
- remotes=remotes,
- **test,
- ) for test in data.pop("tests")
- ]
- return cls(tests=tests, **data)
- def benchmark(self, runner: pyperf.Runner, **kwargs): # pragma: no cover
- for test in self.tests:
- runner.bench_func(
- test.fully_qualified_name,
- partial(test.validate_ignoring_errors, **kwargs),
- )
- @frozen(repr=False)
- class _Test:
- version: Version
- subject: str
- case_description: str
- description: str
- data: Any
- schema: Mapping[str, Any] | bool
- valid: bool
- _remotes: referencing.jsonschema.SchemaRegistry
- comment: str | None = None
- def __repr__(self): # pragma: no cover
- return f"<Test {self.fully_qualified_name}>"
- @property
- def fully_qualified_name(self): # pragma: no cover
- return " > ".join( # noqa: FLY002
- [
- self.version.name,
- self.subject,
- self.case_description,
- self.description,
- ],
- )
- def to_unittest_method(self, skip=lambda test: None, **kwargs):
- if self.valid:
- def fn(this):
- self.validate(**kwargs)
- else:
- def fn(this):
- with this.assertRaises(jsonschema.ValidationError):
- self.validate(**kwargs)
- fn.__name__ = "_".join(
- [
- "test",
- _DELIMITERS.sub("_", self.subject),
- _DELIMITERS.sub("_", self.case_description),
- _DELIMITERS.sub("_", self.description),
- ],
- )
- reason = skip(self)
- if reason is None or os.environ.get("JSON_SCHEMA_DEBUG", "0") != "0":
- return fn
- elif os.environ.get("JSON_SCHEMA_EXPECTED_FAILURES", "0") != "0": # pragma: no cover # noqa: E501
- return unittest.expectedFailure(fn)
- else:
- return unittest.skip(reason)(fn)
- def validate(self, Validator, **kwargs):
- Validator.check_schema(self.schema)
- validator = Validator(
- schema=self.schema,
- registry=self._remotes,
- **kwargs,
- )
- if os.environ.get("JSON_SCHEMA_DEBUG", "0") != "0": # pragma: no cover
- breakpoint() # noqa: T100
- validator.validate(instance=self.data)
- def validate_ignoring_errors(self, Validator): # pragma: no cover
- with suppress(jsonschema.ValidationError):
- self.validate(Validator=Validator)
- def _someone_save_us_the_module_of_the_caller():
- """
- The FQON of the module 2nd stack frames up from here.
- This is intended to allow us to dynamically return test case classes that
- are indistinguishable from being defined in the module that wants them.
- Otherwise, trial will mis-print the FQON, and copy pasting it won't re-run
- the class that really is running.
- Save us all, this is all so so so so so terrible.
- """
- return sys._getframe(2).f_globals["__name__"]
|