_suite.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. """
  2. Python representations of the JSON Schema Test Suite tests.
  3. """
  4. from __future__ import annotations
  5. from contextlib import suppress
  6. from functools import partial
  7. from pathlib import Path
  8. from typing import TYPE_CHECKING, Any
  9. import json
  10. import os
  11. import re
  12. import subprocess
  13. import sys
  14. import unittest
  15. from attrs import field, frozen
  16. from referencing import Registry
  17. import referencing.jsonschema
  18. if TYPE_CHECKING:
  19. from collections.abc import Iterable, Mapping, Sequence
  20. import pyperf
  21. from jsonschema.validators import _VALIDATORS
  22. import jsonschema
  23. _DELIMITERS = re.compile(r"[\W\- ]+")
  24. def _find_suite():
  25. root = os.environ.get("JSON_SCHEMA_TEST_SUITE")
  26. if root is not None:
  27. return Path(root)
  28. root = Path(jsonschema.__file__).parent.parent / "json"
  29. if not root.is_dir(): # pragma: no cover
  30. raise ValueError(
  31. (
  32. "Can't find the JSON-Schema-Test-Suite directory. "
  33. "Set the 'JSON_SCHEMA_TEST_SUITE' environment "
  34. "variable or run the tests from alongside a checkout "
  35. "of the suite."
  36. ),
  37. )
  38. return root
  39. @frozen
  40. class Suite:
  41. _root: Path = field(factory=_find_suite)
  42. _remotes: referencing.jsonschema.SchemaRegistry = field(init=False)
  43. def __attrs_post_init__(self):
  44. jsonschema_suite = self._root.joinpath("bin", "jsonschema_suite")
  45. argv = [sys.executable, str(jsonschema_suite), "remotes"]
  46. remotes = subprocess.check_output(argv).decode("utf-8")
  47. resources = json.loads(remotes)
  48. li = "http://localhost:1234/locationIndependentIdentifierPre2019.json"
  49. li4 = "http://localhost:1234/locationIndependentIdentifierDraft4.json"
  50. registry = Registry().with_resources(
  51. [
  52. (
  53. li,
  54. referencing.jsonschema.DRAFT7.create_resource(
  55. contents=resources.pop(li),
  56. ),
  57. ),
  58. (
  59. li4,
  60. referencing.jsonschema.DRAFT4.create_resource(
  61. contents=resources.pop(li4),
  62. ),
  63. ),
  64. ],
  65. ).with_contents(
  66. resources.items(),
  67. default_specification=referencing.jsonschema.DRAFT202012,
  68. )
  69. object.__setattr__(self, "_remotes", registry)
  70. def benchmark(self, runner: pyperf.Runner): # pragma: no cover
  71. for name, Validator in _VALIDATORS.items():
  72. self.version(name=name).benchmark(
  73. runner=runner,
  74. Validator=Validator,
  75. )
  76. def version(self, name) -> Version:
  77. return Version(
  78. name=name,
  79. path=self._root / "tests" / name,
  80. remotes=self._remotes,
  81. )
  82. @frozen
  83. class Version:
  84. _path: Path
  85. _remotes: referencing.jsonschema.SchemaRegistry
  86. name: str
  87. def benchmark(self, **kwargs): # pragma: no cover
  88. for case in self.cases():
  89. case.benchmark(**kwargs)
  90. def cases(self) -> Iterable[_Case]:
  91. return self._cases_in(paths=self._path.glob("*.json"))
  92. def format_cases(self) -> Iterable[_Case]:
  93. return self._cases_in(paths=self._path.glob("optional/format/*.json"))
  94. def optional_cases_of(self, name: str) -> Iterable[_Case]:
  95. return self._cases_in(paths=[self._path / "optional" / f"{name}.json"])
  96. def to_unittest_testcase(self, *groups, **kwargs):
  97. name = kwargs.pop("name", "Test" + self.name.title().replace("-", ""))
  98. methods = {
  99. method.__name__: method
  100. for method in (
  101. test.to_unittest_method(**kwargs)
  102. for group in groups
  103. for case in group
  104. for test in case.tests
  105. )
  106. }
  107. cls = type(name, (unittest.TestCase,), methods)
  108. # We're doing crazy things, so if they go wrong, like a function
  109. # behaving differently on some other interpreter, just make them
  110. # not happen.
  111. with suppress(Exception):
  112. cls.__module__ = _someone_save_us_the_module_of_the_caller()
  113. return cls
  114. def _cases_in(self, paths: Iterable[Path]) -> Iterable[_Case]:
  115. for path in paths:
  116. for case in json.loads(path.read_text(encoding="utf-8")):
  117. yield _Case.from_dict(
  118. case,
  119. version=self,
  120. subject=path.stem,
  121. remotes=self._remotes,
  122. )
  123. @frozen
  124. class _Case:
  125. version: Version
  126. subject: str
  127. description: str
  128. schema: Mapping[str, Any] | bool
  129. tests: list[_Test]
  130. comment: str | None = None
  131. specification: Sequence[dict[str, str]] = ()
  132. @classmethod
  133. def from_dict(cls, data, remotes, **kwargs):
  134. data.update(kwargs)
  135. tests = [
  136. _Test(
  137. version=data["version"],
  138. subject=data["subject"],
  139. case_description=data["description"],
  140. schema=data["schema"],
  141. remotes=remotes,
  142. **test,
  143. ) for test in data.pop("tests")
  144. ]
  145. return cls(tests=tests, **data)
  146. def benchmark(self, runner: pyperf.Runner, **kwargs): # pragma: no cover
  147. for test in self.tests:
  148. runner.bench_func(
  149. test.fully_qualified_name,
  150. partial(test.validate_ignoring_errors, **kwargs),
  151. )
  152. @frozen(repr=False)
  153. class _Test:
  154. version: Version
  155. subject: str
  156. case_description: str
  157. description: str
  158. data: Any
  159. schema: Mapping[str, Any] | bool
  160. valid: bool
  161. _remotes: referencing.jsonschema.SchemaRegistry
  162. comment: str | None = None
  163. def __repr__(self): # pragma: no cover
  164. return f"<Test {self.fully_qualified_name}>"
  165. @property
  166. def fully_qualified_name(self): # pragma: no cover
  167. return " > ".join( # noqa: FLY002
  168. [
  169. self.version.name,
  170. self.subject,
  171. self.case_description,
  172. self.description,
  173. ],
  174. )
  175. def to_unittest_method(self, skip=lambda test: None, **kwargs):
  176. if self.valid:
  177. def fn(this):
  178. self.validate(**kwargs)
  179. else:
  180. def fn(this):
  181. with this.assertRaises(jsonschema.ValidationError):
  182. self.validate(**kwargs)
  183. fn.__name__ = "_".join(
  184. [
  185. "test",
  186. _DELIMITERS.sub("_", self.subject),
  187. _DELIMITERS.sub("_", self.case_description),
  188. _DELIMITERS.sub("_", self.description),
  189. ],
  190. )
  191. reason = skip(self)
  192. if reason is None or os.environ.get("JSON_SCHEMA_DEBUG", "0") != "0":
  193. return fn
  194. elif os.environ.get("JSON_SCHEMA_EXPECTED_FAILURES", "0") != "0": # pragma: no cover # noqa: E501
  195. return unittest.expectedFailure(fn)
  196. else:
  197. return unittest.skip(reason)(fn)
  198. def validate(self, Validator, **kwargs):
  199. Validator.check_schema(self.schema)
  200. validator = Validator(
  201. schema=self.schema,
  202. registry=self._remotes,
  203. **kwargs,
  204. )
  205. if os.environ.get("JSON_SCHEMA_DEBUG", "0") != "0": # pragma: no cover
  206. breakpoint() # noqa: T100
  207. validator.validate(instance=self.data)
  208. def validate_ignoring_errors(self, Validator): # pragma: no cover
  209. with suppress(jsonschema.ValidationError):
  210. self.validate(Validator=Validator)
  211. def _someone_save_us_the_module_of_the_caller():
  212. """
  213. The FQON of the module 2nd stack frames up from here.
  214. This is intended to allow us to dynamically return test case classes that
  215. are indistinguishable from being defined in the module that wants them.
  216. Otherwise, trial will mis-print the FQON, and copy pasting it won't re-run
  217. the class that really is running.
  218. Save us all, this is all so so so so so terrible.
  219. """
  220. return sys._getframe(2).f_globals["__name__"]