schemathesis 3.13.0__py3-none-any.whl → 4.4.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. schemathesis/__init__.py +53 -25
  2. schemathesis/auths.py +507 -0
  3. schemathesis/checks.py +190 -25
  4. schemathesis/cli/__init__.py +27 -1016
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +133 -0
  7. schemathesis/cli/commands/data.py +10 -0
  8. schemathesis/cli/commands/run/__init__.py +602 -0
  9. schemathesis/cli/commands/run/context.py +228 -0
  10. schemathesis/cli/commands/run/events.py +60 -0
  11. schemathesis/cli/commands/run/executor.py +157 -0
  12. schemathesis/cli/commands/run/filters.py +53 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +45 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
  17. schemathesis/cli/commands/run/handlers/output.py +1750 -0
  18. schemathesis/cli/commands/run/loaders.py +118 -0
  19. schemathesis/cli/commands/run/validation.py +256 -0
  20. schemathesis/cli/constants.py +5 -0
  21. schemathesis/cli/core.py +19 -0
  22. schemathesis/cli/ext/fs.py +16 -0
  23. schemathesis/cli/ext/groups.py +203 -0
  24. schemathesis/cli/ext/options.py +81 -0
  25. schemathesis/config/__init__.py +202 -0
  26. schemathesis/config/_auth.py +51 -0
  27. schemathesis/config/_checks.py +268 -0
  28. schemathesis/config/_diff_base.py +101 -0
  29. schemathesis/config/_env.py +21 -0
  30. schemathesis/config/_error.py +163 -0
  31. schemathesis/config/_generation.py +157 -0
  32. schemathesis/config/_health_check.py +24 -0
  33. schemathesis/config/_operations.py +335 -0
  34. schemathesis/config/_output.py +171 -0
  35. schemathesis/config/_parameters.py +19 -0
  36. schemathesis/config/_phases.py +253 -0
  37. schemathesis/config/_projects.py +543 -0
  38. schemathesis/config/_rate_limit.py +17 -0
  39. schemathesis/config/_report.py +120 -0
  40. schemathesis/config/_validator.py +9 -0
  41. schemathesis/config/_warnings.py +89 -0
  42. schemathesis/config/schema.json +975 -0
  43. schemathesis/core/__init__.py +72 -0
  44. schemathesis/core/adapter.py +34 -0
  45. schemathesis/core/compat.py +32 -0
  46. schemathesis/core/control.py +2 -0
  47. schemathesis/core/curl.py +100 -0
  48. schemathesis/core/deserialization.py +210 -0
  49. schemathesis/core/errors.py +588 -0
  50. schemathesis/core/failures.py +316 -0
  51. schemathesis/core/fs.py +19 -0
  52. schemathesis/core/hooks.py +20 -0
  53. schemathesis/core/jsonschema/__init__.py +13 -0
  54. schemathesis/core/jsonschema/bundler.py +183 -0
  55. schemathesis/core/jsonschema/keywords.py +40 -0
  56. schemathesis/core/jsonschema/references.py +222 -0
  57. schemathesis/core/jsonschema/types.py +41 -0
  58. schemathesis/core/lazy_import.py +15 -0
  59. schemathesis/core/loaders.py +107 -0
  60. schemathesis/core/marks.py +66 -0
  61. schemathesis/core/media_types.py +79 -0
  62. schemathesis/core/output/__init__.py +46 -0
  63. schemathesis/core/output/sanitization.py +54 -0
  64. schemathesis/core/parameters.py +45 -0
  65. schemathesis/core/rate_limit.py +60 -0
  66. schemathesis/core/registries.py +34 -0
  67. schemathesis/core/result.py +27 -0
  68. schemathesis/core/schema_analysis.py +17 -0
  69. schemathesis/core/shell.py +203 -0
  70. schemathesis/core/transforms.py +144 -0
  71. schemathesis/core/transport.py +223 -0
  72. schemathesis/core/validation.py +73 -0
  73. schemathesis/core/version.py +7 -0
  74. schemathesis/engine/__init__.py +28 -0
  75. schemathesis/engine/context.py +152 -0
  76. schemathesis/engine/control.py +44 -0
  77. schemathesis/engine/core.py +201 -0
  78. schemathesis/engine/errors.py +446 -0
  79. schemathesis/engine/events.py +284 -0
  80. schemathesis/engine/observations.py +42 -0
  81. schemathesis/engine/phases/__init__.py +108 -0
  82. schemathesis/engine/phases/analysis.py +28 -0
  83. schemathesis/engine/phases/probes.py +172 -0
  84. schemathesis/engine/phases/stateful/__init__.py +68 -0
  85. schemathesis/engine/phases/stateful/_executor.py +364 -0
  86. schemathesis/engine/phases/stateful/context.py +85 -0
  87. schemathesis/engine/phases/unit/__init__.py +220 -0
  88. schemathesis/engine/phases/unit/_executor.py +459 -0
  89. schemathesis/engine/phases/unit/_pool.py +82 -0
  90. schemathesis/engine/recorder.py +254 -0
  91. schemathesis/errors.py +47 -0
  92. schemathesis/filters.py +395 -0
  93. schemathesis/generation/__init__.py +25 -0
  94. schemathesis/generation/case.py +478 -0
  95. schemathesis/generation/coverage.py +1528 -0
  96. schemathesis/generation/hypothesis/__init__.py +121 -0
  97. schemathesis/generation/hypothesis/builder.py +992 -0
  98. schemathesis/generation/hypothesis/examples.py +56 -0
  99. schemathesis/generation/hypothesis/given.py +66 -0
  100. schemathesis/generation/hypothesis/reporting.py +285 -0
  101. schemathesis/generation/meta.py +227 -0
  102. schemathesis/generation/metrics.py +93 -0
  103. schemathesis/generation/modes.py +20 -0
  104. schemathesis/generation/overrides.py +127 -0
  105. schemathesis/generation/stateful/__init__.py +37 -0
  106. schemathesis/generation/stateful/state_machine.py +294 -0
  107. schemathesis/graphql/__init__.py +15 -0
  108. schemathesis/graphql/checks.py +109 -0
  109. schemathesis/graphql/loaders.py +285 -0
  110. schemathesis/hooks.py +270 -91
  111. schemathesis/openapi/__init__.py +13 -0
  112. schemathesis/openapi/checks.py +467 -0
  113. schemathesis/openapi/generation/__init__.py +0 -0
  114. schemathesis/openapi/generation/filters.py +72 -0
  115. schemathesis/openapi/loaders.py +315 -0
  116. schemathesis/pytest/__init__.py +5 -0
  117. schemathesis/pytest/control_flow.py +7 -0
  118. schemathesis/pytest/lazy.py +341 -0
  119. schemathesis/pytest/loaders.py +36 -0
  120. schemathesis/pytest/plugin.py +357 -0
  121. schemathesis/python/__init__.py +0 -0
  122. schemathesis/python/asgi.py +12 -0
  123. schemathesis/python/wsgi.py +12 -0
  124. schemathesis/schemas.py +683 -247
  125. schemathesis/specs/graphql/__init__.py +0 -1
  126. schemathesis/specs/graphql/nodes.py +27 -0
  127. schemathesis/specs/graphql/scalars.py +86 -0
  128. schemathesis/specs/graphql/schemas.py +395 -123
  129. schemathesis/specs/graphql/validation.py +33 -0
  130. schemathesis/specs/openapi/__init__.py +9 -1
  131. schemathesis/specs/openapi/_hypothesis.py +578 -317
  132. schemathesis/specs/openapi/adapter/__init__.py +10 -0
  133. schemathesis/specs/openapi/adapter/parameters.py +729 -0
  134. schemathesis/specs/openapi/adapter/protocol.py +59 -0
  135. schemathesis/specs/openapi/adapter/references.py +19 -0
  136. schemathesis/specs/openapi/adapter/responses.py +368 -0
  137. schemathesis/specs/openapi/adapter/security.py +144 -0
  138. schemathesis/specs/openapi/adapter/v2.py +30 -0
  139. schemathesis/specs/openapi/adapter/v3_0.py +30 -0
  140. schemathesis/specs/openapi/adapter/v3_1.py +30 -0
  141. schemathesis/specs/openapi/analysis.py +96 -0
  142. schemathesis/specs/openapi/checks.py +753 -74
  143. schemathesis/specs/openapi/converter.py +176 -37
  144. schemathesis/specs/openapi/definitions.py +599 -4
  145. schemathesis/specs/openapi/examples.py +581 -165
  146. schemathesis/specs/openapi/expressions/__init__.py +52 -5
  147. schemathesis/specs/openapi/expressions/extractors.py +25 -0
  148. schemathesis/specs/openapi/expressions/lexer.py +34 -31
  149. schemathesis/specs/openapi/expressions/nodes.py +97 -46
  150. schemathesis/specs/openapi/expressions/parser.py +35 -13
  151. schemathesis/specs/openapi/formats.py +122 -0
  152. schemathesis/specs/openapi/media_types.py +75 -0
  153. schemathesis/specs/openapi/negative/__init__.py +117 -68
  154. schemathesis/specs/openapi/negative/mutations.py +294 -104
  155. schemathesis/specs/openapi/negative/utils.py +3 -6
  156. schemathesis/specs/openapi/patterns.py +458 -0
  157. schemathesis/specs/openapi/references.py +60 -81
  158. schemathesis/specs/openapi/schemas.py +648 -650
  159. schemathesis/specs/openapi/serialization.py +53 -30
  160. schemathesis/specs/openapi/stateful/__init__.py +404 -69
  161. schemathesis/specs/openapi/stateful/control.py +87 -0
  162. schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
  163. schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
  164. schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
  165. schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
  166. schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
  167. schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
  168. schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
  169. schemathesis/specs/openapi/stateful/inference.py +254 -0
  170. schemathesis/specs/openapi/stateful/links.py +219 -78
  171. schemathesis/specs/openapi/types/__init__.py +3 -0
  172. schemathesis/specs/openapi/types/common.py +23 -0
  173. schemathesis/specs/openapi/types/v2.py +129 -0
  174. schemathesis/specs/openapi/types/v3.py +134 -0
  175. schemathesis/specs/openapi/utils.py +7 -6
  176. schemathesis/specs/openapi/warnings.py +75 -0
  177. schemathesis/transport/__init__.py +224 -0
  178. schemathesis/transport/asgi.py +26 -0
  179. schemathesis/transport/prepare.py +126 -0
  180. schemathesis/transport/requests.py +278 -0
  181. schemathesis/transport/serialization.py +329 -0
  182. schemathesis/transport/wsgi.py +175 -0
  183. schemathesis-4.4.2.dist-info/METADATA +213 -0
  184. schemathesis-4.4.2.dist-info/RECORD +192 -0
  185. {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
  186. schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
  187. {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
  188. schemathesis/_compat.py +0 -41
  189. schemathesis/_hypothesis.py +0 -115
  190. schemathesis/cli/callbacks.py +0 -188
  191. schemathesis/cli/cassettes.py +0 -253
  192. schemathesis/cli/context.py +0 -36
  193. schemathesis/cli/debug.py +0 -21
  194. schemathesis/cli/handlers.py +0 -11
  195. schemathesis/cli/junitxml.py +0 -41
  196. schemathesis/cli/options.py +0 -51
  197. schemathesis/cli/output/__init__.py +0 -1
  198. schemathesis/cli/output/default.py +0 -508
  199. schemathesis/cli/output/short.py +0 -40
  200. schemathesis/constants.py +0 -79
  201. schemathesis/exceptions.py +0 -207
  202. schemathesis/extra/_aiohttp.py +0 -27
  203. schemathesis/extra/_flask.py +0 -10
  204. schemathesis/extra/_server.py +0 -16
  205. schemathesis/extra/pytest_plugin.py +0 -216
  206. schemathesis/failures.py +0 -131
  207. schemathesis/fixups/__init__.py +0 -29
  208. schemathesis/fixups/fast_api.py +0 -30
  209. schemathesis/lazy.py +0 -227
  210. schemathesis/models.py +0 -1041
  211. schemathesis/parameters.py +0 -88
  212. schemathesis/runner/__init__.py +0 -460
  213. schemathesis/runner/events.py +0 -240
  214. schemathesis/runner/impl/__init__.py +0 -3
  215. schemathesis/runner/impl/core.py +0 -755
  216. schemathesis/runner/impl/solo.py +0 -85
  217. schemathesis/runner/impl/threadpool.py +0 -367
  218. schemathesis/runner/serialization.py +0 -189
  219. schemathesis/serializers.py +0 -233
  220. schemathesis/service/__init__.py +0 -3
  221. schemathesis/service/client.py +0 -46
  222. schemathesis/service/constants.py +0 -12
  223. schemathesis/service/events.py +0 -39
  224. schemathesis/service/handler.py +0 -39
  225. schemathesis/service/models.py +0 -7
  226. schemathesis/service/serialization.py +0 -153
  227. schemathesis/service/worker.py +0 -40
  228. schemathesis/specs/graphql/loaders.py +0 -215
  229. schemathesis/specs/openapi/constants.py +0 -7
  230. schemathesis/specs/openapi/expressions/context.py +0 -12
  231. schemathesis/specs/openapi/expressions/pointers.py +0 -29
  232. schemathesis/specs/openapi/filters.py +0 -44
  233. schemathesis/specs/openapi/links.py +0 -302
  234. schemathesis/specs/openapi/loaders.py +0 -453
  235. schemathesis/specs/openapi/parameters.py +0 -413
  236. schemathesis/specs/openapi/security.py +0 -129
  237. schemathesis/specs/openapi/validation.py +0 -24
  238. schemathesis/stateful.py +0 -349
  239. schemathesis/targets.py +0 -32
  240. schemathesis/types.py +0 -38
  241. schemathesis/utils.py +0 -436
  242. schemathesis-3.13.0.dist-info/METADATA +0 -202
  243. schemathesis-3.13.0.dist-info/RECORD +0 -91
  244. schemathesis-3.13.0.dist-info/entry_points.txt +0 -6
  245. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
@@ -0,0 +1,1528 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ import re
5
+ from contextlib import contextmanager, suppress
6
+ from dataclasses import dataclass
7
+ from functools import lru_cache, partial
8
+ from itertools import combinations
9
+
10
+ from schemathesis.core.jsonschema.bundler import BUNDLE_STORAGE_KEY
11
+ from schemathesis.core.jsonschema.keywords import ALL_KEYWORDS
12
+
13
+ try:
14
+ from json.encoder import _make_iterencode # type: ignore[attr-defined]
15
+ except ImportError:
16
+ _make_iterencode = None
17
+
18
+ try:
19
+ from json.encoder import c_make_encoder # type: ignore[attr-defined]
20
+ except ImportError:
21
+ c_make_encoder = None
22
+
23
+ from json.encoder import JSONEncoder, encode_basestring_ascii
24
+ from typing import Any, Callable, Generator, Iterator, TypeVar, cast
25
+ from urllib.parse import quote_plus
26
+
27
+ import jsonschema.protocols
28
+ from hypothesis import strategies as st
29
+ from hypothesis.errors import InvalidArgument, Unsatisfiable
30
+ from hypothesis_jsonschema import from_schema
31
+ from hypothesis_jsonschema._canonicalise import canonicalish
32
+ from hypothesis_jsonschema._from_schema import STRING_FORMATS as BUILT_IN_STRING_FORMATS
33
+
34
+ from schemathesis.core import INTERNAL_BUFFER_SIZE, NOT_SET
35
+ from schemathesis.core.compat import RefResolutionError, RefResolver
36
+ from schemathesis.core.parameters import ParameterLocation
37
+ from schemathesis.core.transforms import deepclone
38
+ from schemathesis.core.validation import contains_unicode_surrogate_pair, has_invalid_characters, is_latin_1_encodable
39
+ from schemathesis.generation import GenerationMode
40
+ from schemathesis.generation.hypothesis import examples
41
+ from schemathesis.generation.meta import CoverageScenario
42
+ from schemathesis.openapi.generation.filters import is_invalid_path_parameter
43
+
44
+ from ..specs.openapi.converter import update_pattern_in_schema
45
+ from ..specs.openapi.formats import STRING_FORMATS, get_default_format_strategies
46
+ from ..specs.openapi.patterns import update_quantifier
47
+
48
+
49
+ def _replace_zero_with_nonzero(x: float) -> float:
50
+ return x or 0.0
51
+
52
+
53
+ def json_recursive_strategy(strategy: st.SearchStrategy) -> st.SearchStrategy:
54
+ return st.lists(strategy, max_size=2) | st.dictionaries(st.text(), strategy, max_size=2)
55
+
56
+
57
+ NEGATIVE_MODE_MAX_LENGTH_WITH_PATTERN = 100
58
+ NEGATIVE_MODE_MAX_ITEMS = 15
59
+ FLOAT_STRATEGY: st.SearchStrategy = st.floats(allow_nan=False, allow_infinity=False).map(_replace_zero_with_nonzero)
60
+ NUMERIC_STRATEGY: st.SearchStrategy = st.integers() | FLOAT_STRATEGY
61
+ JSON_STRATEGY: st.SearchStrategy = st.recursive(
62
+ st.none() | st.booleans() | NUMERIC_STRATEGY | st.text(max_size=16),
63
+ json_recursive_strategy,
64
+ max_leaves=2,
65
+ )
66
+ ARRAY_STRATEGY: st.SearchStrategy = st.lists(JSON_STRATEGY, min_size=2, max_size=3)
67
+ OBJECT_STRATEGY: st.SearchStrategy = st.dictionaries(st.text(max_size=16), JSON_STRATEGY, max_size=2)
68
+
69
+
70
+ STRATEGIES_FOR_TYPE = {
71
+ "integer": st.integers(),
72
+ "number": NUMERIC_STRATEGY,
73
+ "boolean": st.booleans(),
74
+ "null": st.none(),
75
+ "string": st.text(),
76
+ "array": ARRAY_STRATEGY,
77
+ "object": OBJECT_STRATEGY,
78
+ }
79
+
80
+
81
+ def get_strategy_for_type(ty: str | list[str]) -> st.SearchStrategy:
82
+ if isinstance(ty, str):
83
+ return STRATEGIES_FOR_TYPE[ty]
84
+ return st.one_of(STRATEGIES_FOR_TYPE[t] for t in ty if t in STRATEGIES_FOR_TYPE)
85
+
86
+
87
+ FORMAT_STRATEGIES = {**BUILT_IN_STRING_FORMATS, **get_default_format_strategies(), **STRING_FORMATS}
88
+
89
+ UNKNOWN_PROPERTY_KEY = "x-schemathesis-unknown-property"
90
+ UNKNOWN_PROPERTY_VALUE = 42
91
+
92
+
93
+ @dataclass
94
+ class GeneratedValue:
95
+ value: Any
96
+ generation_mode: GenerationMode
97
+ scenario: CoverageScenario
98
+ description: str
99
+ parameter: str | None
100
+ location: str | None
101
+
102
+ __slots__ = ("value", "generation_mode", "scenario", "description", "parameter", "location")
103
+
104
+ @classmethod
105
+ def with_positive(cls, value: Any, *, scenario: CoverageScenario, description: str) -> GeneratedValue:
106
+ return cls(
107
+ value=value,
108
+ generation_mode=GenerationMode.POSITIVE,
109
+ scenario=scenario,
110
+ description=description,
111
+ location=None,
112
+ parameter=None,
113
+ )
114
+
115
+ @classmethod
116
+ def with_negative(
117
+ cls, value: Any, *, scenario: CoverageScenario, description: str, location: str, parameter: str | None = None
118
+ ) -> GeneratedValue:
119
+ return cls(
120
+ value=value,
121
+ generation_mode=GenerationMode.NEGATIVE,
122
+ scenario=scenario,
123
+ description=description,
124
+ location=location,
125
+ parameter=parameter,
126
+ )
127
+
128
+
129
+ PositiveValue = GeneratedValue.with_positive
130
+ NegativeValue = GeneratedValue.with_negative
131
+
132
+
133
+ @lru_cache(maxsize=128)
134
+ def cached_draw(strategy: st.SearchStrategy) -> Any:
135
+ return examples.generate_one(strategy)
136
+
137
+
138
+ @dataclass
139
+ class CoverageContext:
140
+ root_schema: dict[str, Any]
141
+ generation_modes: list[GenerationMode]
142
+ location: ParameterLocation
143
+ media_type: tuple[str, str] | None
144
+ is_required: bool
145
+ path: list[str | int]
146
+ custom_formats: dict[str, st.SearchStrategy]
147
+ validator_cls: type[jsonschema.protocols.Validator]
148
+ _resolver: RefResolver | None
149
+ allow_extra_parameters: bool
150
+
151
+ __slots__ = (
152
+ "root_schema",
153
+ "location",
154
+ "media_type",
155
+ "generation_modes",
156
+ "is_required",
157
+ "path",
158
+ "custom_formats",
159
+ "validator_cls",
160
+ "_resolver",
161
+ "allow_extra_parameters",
162
+ )
163
+
164
+ def __init__(
165
+ self,
166
+ *,
167
+ root_schema: dict[str, Any],
168
+ location: ParameterLocation,
169
+ media_type: tuple[str, str] | None,
170
+ generation_modes: list[GenerationMode] | None = None,
171
+ is_required: bool,
172
+ path: list[str | int] | None = None,
173
+ custom_formats: dict[str, st.SearchStrategy],
174
+ validator_cls: type[jsonschema.protocols.Validator],
175
+ _resolver: RefResolver | None = None,
176
+ allow_extra_parameters: bool = True,
177
+ ) -> None:
178
+ self.root_schema = root_schema
179
+ self.location = location
180
+ self.media_type = media_type
181
+ self.generation_modes = generation_modes if generation_modes is not None else list(GenerationMode)
182
+ self.is_required = is_required
183
+ self.path = path or []
184
+ self.custom_formats = custom_formats
185
+ self.validator_cls = validator_cls
186
+ self._resolver = _resolver
187
+ self.allow_extra_parameters = allow_extra_parameters
188
+
189
+ @property
190
+ def resolver(self) -> RefResolver:
191
+ """Lazy-initialized cached resolver."""
192
+ if self._resolver is None:
193
+ self._resolver = RefResolver.from_schema(self.root_schema)
194
+ return cast(RefResolver, self._resolver)
195
+
196
+ def resolve_ref(self, ref: str) -> dict | bool:
197
+ """Resolve a $ref to its schema definition."""
198
+ _, resolved = self.resolver.resolve(ref)
199
+ return resolved
200
+
201
+ @contextmanager
202
+ def at(self, key: str | int) -> Generator[None, None, None]:
203
+ self.path.append(key)
204
+ try:
205
+ yield
206
+ finally:
207
+ self.path.pop()
208
+
209
+ @property
210
+ def current_path(self) -> str:
211
+ return "/" + "/".join(str(key) for key in self.path)
212
+
213
+ def with_positive(self) -> CoverageContext:
214
+ return CoverageContext(
215
+ root_schema=self.root_schema,
216
+ location=self.location,
217
+ media_type=self.media_type,
218
+ generation_modes=[GenerationMode.POSITIVE],
219
+ is_required=self.is_required,
220
+ path=self.path,
221
+ custom_formats=self.custom_formats,
222
+ validator_cls=self.validator_cls,
223
+ _resolver=self._resolver,
224
+ allow_extra_parameters=self.allow_extra_parameters,
225
+ )
226
+
227
+ def with_negative(self) -> CoverageContext:
228
+ return CoverageContext(
229
+ root_schema=self.root_schema,
230
+ location=self.location,
231
+ media_type=self.media_type,
232
+ generation_modes=[GenerationMode.NEGATIVE],
233
+ is_required=self.is_required,
234
+ path=self.path,
235
+ custom_formats=self.custom_formats,
236
+ validator_cls=self.validator_cls,
237
+ _resolver=self._resolver,
238
+ allow_extra_parameters=self.allow_extra_parameters,
239
+ )
240
+
241
+ def is_valid_for_location(self, value: Any) -> bool:
242
+ if self.location in ("header", "cookie") and isinstance(value, str):
243
+ return not value or (is_latin_1_encodable(value) and not has_invalid_characters("", value))
244
+ elif self.location == "path":
245
+ return not is_invalid_path_parameter(value)
246
+ return True
247
+
248
+ def leads_to_negative_test_case(self, value: Any) -> bool:
249
+ if self.location == "query":
250
+ # Some values will not be serialized into the query string
251
+ if isinstance(value, list) and not self.is_required:
252
+ # Optional parameters should be present
253
+ return any(item not in [{}, []] for item in value)
254
+ return True
255
+
256
+ def will_be_serialized_to_string(self) -> bool:
257
+ return self.location in ("query", "path", "header", "cookie") or (
258
+ self.location == "body"
259
+ and self.media_type
260
+ in frozenset(
261
+ [
262
+ ("multipart", "form-data"),
263
+ ("application", "x-www-form-urlencoded"),
264
+ ]
265
+ )
266
+ )
267
+
268
+ def can_be_negated(self, schema: dict[str, Any]) -> bool:
269
+ # Path, query, header, and cookie parameters will be stringified anyway
270
+ # If there are no constraints, then anything will match the original schema after serialization
271
+ if self.will_be_serialized_to_string():
272
+ cleaned = {
273
+ k: v
274
+ for k, v in schema.items()
275
+ if not k.startswith("x-") and k not in ["description", "example", "examples"]
276
+ }
277
+ return cleaned not in [{}, {"type": "string"}]
278
+ return True
279
+
280
+ def generate_from(self, strategy: st.SearchStrategy) -> Any:
281
+ return cached_draw(strategy)
282
+
283
+ def generate_from_schema(self, schema: dict | bool) -> Any:
284
+ if isinstance(schema, dict) and "$ref" in schema:
285
+ reference = schema["$ref"]
286
+ # Deep clone to avoid circular references in Python objects
287
+ schema = deepclone(self.resolve_ref(reference))
288
+ if isinstance(schema, bool):
289
+ return 0
290
+ keys = sorted([k for k in schema if not k.startswith("x-") and k not in ["description", "example", "examples"]])
291
+ if keys == ["type"]:
292
+ return cached_draw(get_strategy_for_type(schema["type"]))
293
+ if keys == ["format", "type"]:
294
+ if schema["type"] != "string":
295
+ return cached_draw(get_strategy_for_type(schema["type"]))
296
+ elif schema["format"] in FORMAT_STRATEGIES:
297
+ return cached_draw(FORMAT_STRATEGIES[schema["format"]])
298
+ if (keys == ["maxLength", "minLength", "type"] or keys == ["maxLength", "type"]) and schema["type"] == "string":
299
+ return cached_draw(st.text(min_size=schema.get("minLength", 0), max_size=schema["maxLength"]))
300
+ if (
301
+ keys == ["properties", "required", "type"]
302
+ or keys == ["properties", "required"]
303
+ or keys == ["properties", "type"]
304
+ or keys == ["properties"]
305
+ ):
306
+ obj = {}
307
+ for key, sub_schema in schema["properties"].items():
308
+ if isinstance(sub_schema, dict) and "const" in sub_schema:
309
+ obj[key] = sub_schema["const"]
310
+ else:
311
+ obj[key] = self.generate_from_schema(sub_schema)
312
+ return obj
313
+ if (
314
+ keys == ["maximum", "minimum", "type"] or keys == ["maximum", "type"] or keys == ["minimum", "type"]
315
+ ) and schema["type"] == "integer":
316
+ return cached_draw(st.integers(min_value=schema.get("minimum"), max_value=schema.get("maximum")))
317
+ if "enum" in schema:
318
+ return cached_draw(st.sampled_from(schema["enum"]))
319
+ if keys == ["multipleOf", "type"] and schema["type"] in ("integer", "number"):
320
+ step = schema["multipleOf"]
321
+ return cached_draw(st.integers().map(step.__mul__))
322
+ if "pattern" in schema:
323
+ pattern = schema["pattern"]
324
+ try:
325
+ re.compile(pattern)
326
+ except re.error:
327
+ raise Unsatisfiable from None
328
+ if "minLength" in schema or "maxLength" in schema:
329
+ min_length = schema.get("minLength")
330
+ max_length = schema.get("maxLength")
331
+ pattern = update_quantifier(pattern, min_length, max_length)
332
+ return cached_draw(st.from_regex(pattern))
333
+ if (keys == ["items", "type"] or keys == ["items", "minItems", "type"]) and isinstance(schema["items"], dict):
334
+ items = schema["items"]
335
+ min_items = schema.get("minItems", 0)
336
+ if "enum" in items:
337
+ return cached_draw(st.lists(st.sampled_from(items["enum"]), min_size=min_items))
338
+ sub_keys = sorted([k for k in items if not k.startswith("x-") and k not in ["description", "example"]])
339
+ if sub_keys == ["type"] and items["type"] == "string":
340
+ return cached_draw(st.lists(st.text(), min_size=min_items))
341
+ if (
342
+ sub_keys == ["properties", "required", "type"]
343
+ or sub_keys == ["properties", "type"]
344
+ or sub_keys == ["properties"]
345
+ ):
346
+ return cached_draw(
347
+ st.lists(
348
+ st.fixed_dictionaries(
349
+ {
350
+ key: from_schema(sub_schema, custom_formats=self.custom_formats)
351
+ for key, sub_schema in items["properties"].items()
352
+ }
353
+ ),
354
+ min_size=min_items,
355
+ )
356
+ )
357
+
358
+ if keys == ["allOf"]:
359
+ for idx, sub_schema in enumerate(schema["allOf"]):
360
+ if "$ref" in sub_schema:
361
+ schema["allOf"][idx] = self.resolve_ref(sub_schema["$ref"])
362
+
363
+ schema = canonicalish(schema)
364
+ if isinstance(schema, dict) and "allOf" not in schema:
365
+ return self.generate_from_schema(schema)
366
+
367
+ if isinstance(schema, dict) and "examples" in schema:
368
+ # Examples may contain binary data which will fail the canonicalisation process in `hypothesis-jsonschema`
369
+ schema = {key: value for key, value in schema.items() if key != "examples"}
370
+ # Prevent some hard to satisfy schemas
371
+ if isinstance(schema, dict) and schema.get("additionalProperties") is False and "required" in schema:
372
+ # Set required properties to any value to simplify generation
373
+ schema = dict(schema)
374
+ properties = schema.setdefault("properties", {})
375
+ for key in schema["required"]:
376
+ properties.setdefault(key, {})
377
+
378
+ # Add bundled schemas if any
379
+ if isinstance(schema, dict) and BUNDLE_STORAGE_KEY in self.root_schema:
380
+ schema = dict(schema)
381
+ schema[BUNDLE_STORAGE_KEY] = self.root_schema[BUNDLE_STORAGE_KEY]
382
+
383
+ return self.generate_from(from_schema(schema, custom_formats=self.custom_formats))
384
+
385
+
386
+ T = TypeVar("T")
387
+
388
+
389
+ if c_make_encoder is not None:
390
+ _iterencode = c_make_encoder(None, None, encode_basestring_ascii, None, ":", ",", True, False, False)
391
+ elif _make_iterencode is not None:
392
+ _iterencode = _make_iterencode(
393
+ None, None, encode_basestring_ascii, None, float.__repr__, ":", ",", True, False, True
394
+ )
395
+ else:
396
+ encoder = JSONEncoder(skipkeys=False, sort_keys=False, indent=None, separators=(":", ","))
397
+ _iterencode = encoder.iterencode
398
+
399
+
400
+ def _encode(o: Any) -> str:
401
+ return "".join(_iterencode(o, False))
402
+
403
+
404
+ def _to_hashable_key(value: T, _encode: Callable = _encode) -> tuple[type, str | T]:
405
+ if isinstance(value, (dict, list)):
406
+ serialized = _encode(value)
407
+ return (type(value), serialized)
408
+ return (type(value), value)
409
+
410
+
411
+ class HashSet:
412
+ """Helper to track already generated values."""
413
+
414
+ __slots__ = ("_data",)
415
+
416
+ def __init__(self) -> None:
417
+ self._data: set[tuple] = set()
418
+
419
+ def insert(self, value: Any) -> bool:
420
+ key = _to_hashable_key(value)
421
+ before = len(self._data)
422
+ self._data.add(key)
423
+ return len(self._data) > before
424
+
425
+ def clear(self) -> None:
426
+ self._data.clear()
427
+
428
+
429
+ def _cover_positive_for_type(
430
+ ctx: CoverageContext, schema: dict, ty: str | None, seen: HashSet | None = None
431
+ ) -> Generator[GeneratedValue, None, None]:
432
+ if ty == "object" or ty == "array":
433
+ template_schema = _get_template_schema(schema, ty)
434
+ template = ctx.generate_from_schema(template_schema)
435
+ elif "properties" in schema or "required" in schema:
436
+ template_schema = _get_template_schema(schema, "object")
437
+ template = ctx.generate_from_schema(template_schema)
438
+ else:
439
+ template = None
440
+ if GenerationMode.POSITIVE in ctx.generation_modes:
441
+ ctx = ctx.with_positive()
442
+ enum = schema.get("enum", NOT_SET)
443
+ const = schema.get("const", NOT_SET)
444
+ for key in ("anyOf", "oneOf"):
445
+ sub_schemas = schema.get(key)
446
+ if sub_schemas is not None:
447
+ for sub_schema in sub_schemas:
448
+ yield from cover_schema_iter(ctx, sub_schema)
449
+ all_of = schema.get("allOf")
450
+ if all_of is not None:
451
+ if len(all_of) == 1:
452
+ yield from cover_schema_iter(ctx, all_of[0])
453
+ else:
454
+ with suppress(jsonschema.SchemaError):
455
+ for idx, sub_schema in enumerate(all_of):
456
+ if "$ref" in sub_schema:
457
+ all_of[idx] = ctx.resolve_ref(sub_schema["$ref"])
458
+ canonical = canonicalish(schema)
459
+ yield from cover_schema_iter(ctx, canonical)
460
+ if enum is not NOT_SET:
461
+ for value in enum:
462
+ yield PositiveValue(value, scenario=CoverageScenario.ENUM_VALUE, description="Enum value")
463
+ elif const is not NOT_SET:
464
+ yield PositiveValue(const, scenario=CoverageScenario.CONST_VALUE, description="Const value")
465
+ elif ty is not None:
466
+ if ty == "null":
467
+ yield PositiveValue(None, scenario=CoverageScenario.NULL_VALUE, description="Value null value")
468
+ elif ty == "boolean":
469
+ yield PositiveValue(True, scenario=CoverageScenario.VALID_BOOLEAN, description="Valid boolean value")
470
+ yield PositiveValue(False, scenario=CoverageScenario.VALID_BOOLEAN, description="Valid boolean value")
471
+ elif ty == "string":
472
+ yield from _positive_string(ctx, schema)
473
+ elif ty == "integer" or ty == "number":
474
+ yield from _positive_number(ctx, schema)
475
+ elif ty == "array":
476
+ yield from _positive_array(ctx, schema, cast(list, template))
477
+ elif ty == "object":
478
+ yield from _positive_object(ctx, schema, cast(dict, template))
479
+ elif "properties" in schema or "required" in schema:
480
+ yield from _positive_object(ctx, schema, cast(dict, template))
481
+ elif "not" in schema and isinstance(schema["not"], (dict, bool)):
482
+ # For 'not' schemas: generate negative cases of inner schema (violations)
483
+ # These violations are positive for the outer schema, so flip the mode
484
+ nctx = ctx.with_negative()
485
+ yield from _flip_generation_mode_for_not(cover_schema_iter(nctx, schema["not"], seen))
486
+
487
+
488
+ @contextmanager
489
+ def _ignore_unfixable(
490
+ *,
491
+ # Cache exception types here as `jsonschema` uses a custom `__getattr__` on the module level
492
+ # and it may cause errors during the interpreter shutdown
493
+ ref_error: type[Exception] = RefResolutionError,
494
+ schema_error: type[Exception] = jsonschema.SchemaError,
495
+ ) -> Generator:
496
+ try:
497
+ yield
498
+ except (Unsatisfiable, ref_error, schema_error):
499
+ pass
500
+ except InvalidArgument as exc:
501
+ message = str(exc)
502
+ if "Cannot create non-empty" not in message and "is not in the specified alphabet" not in message:
503
+ raise
504
+ except TypeError as exc:
505
+ if "first argument must be string or compiled pattern" not in str(exc):
506
+ raise
507
+
508
+
509
+ def cover_schema_iter(
510
+ ctx: CoverageContext, schema: dict | bool, seen: HashSet | None = None
511
+ ) -> Generator[GeneratedValue, None, None]:
512
+ if seen is None:
513
+ seen = HashSet()
514
+
515
+ if isinstance(schema, dict) and "$ref" in schema:
516
+ reference = schema["$ref"]
517
+ try:
518
+ resolved = ctx.resolve_ref(reference)
519
+ if isinstance(resolved, dict):
520
+ schema = {**resolved, **{k: v for k, v in schema.items() if k != "$ref"}}
521
+ yield from cover_schema_iter(ctx, schema, seen)
522
+ else:
523
+ yield from cover_schema_iter(ctx, resolved, seen)
524
+ return
525
+ except RefResolutionError:
526
+ # Can't resolve a reference - at this point, we can't generate anything useful as `$ref` is in the current schema root
527
+ return
528
+
529
+ if schema is True:
530
+ types = ["null", "boolean", "string", "number", "array", "object"]
531
+ schema = {}
532
+ elif schema is False:
533
+ types = []
534
+ schema = {"not": {}}
535
+ elif not any(k in ALL_KEYWORDS for k in schema):
536
+ types = ["null", "boolean", "string", "number", "array", "object"]
537
+ else:
538
+ types = schema.get("type", [])
539
+ push_examples_to_properties(schema)
540
+ if not isinstance(types, list):
541
+ types = [types] # type: ignore[unreachable]
542
+ if not types:
543
+ with _ignore_unfixable():
544
+ yield from _cover_positive_for_type(ctx, schema, None)
545
+ for ty in types:
546
+ with _ignore_unfixable():
547
+ yield from _cover_positive_for_type(ctx, schema, ty)
548
+ if GenerationMode.NEGATIVE in ctx.generation_modes:
549
+ template = None
550
+ if not ctx.can_be_negated(schema):
551
+ return
552
+ for key, value in schema.items():
553
+ with _ignore_unfixable(), ctx.at(key):
554
+ if key == "enum":
555
+ yield from _negative_enum(ctx, value, seen)
556
+ elif key == "const":
557
+ for value_ in _negative_enum(ctx, [value], seen):
558
+ yield value_
559
+ elif key == "type":
560
+ yield from _negative_type(ctx, value, seen, schema)
561
+ elif key == "properties":
562
+ template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
563
+ yield from _negative_properties(ctx, template, value)
564
+ elif key == "patternProperties":
565
+ template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
566
+ yield from _negative_pattern_properties(ctx, template, value)
567
+ elif key == "items" and isinstance(value, dict):
568
+ yield from _negative_items(ctx, value)
569
+ elif key == "pattern":
570
+ min_length = schema.get("minLength")
571
+ max_length = schema.get("maxLength")
572
+ yield from _negative_pattern(ctx, value, min_length=min_length, max_length=max_length)
573
+ elif key == "format" and ("string" in types or not types):
574
+ yield from _negative_format(ctx, schema, value)
575
+ elif key == "maximum":
576
+ next = value + 1
577
+ if seen.insert(next):
578
+ yield NegativeValue(
579
+ next,
580
+ scenario=CoverageScenario.VALUE_ABOVE_MAXIMUM,
581
+ description="Value greater than maximum",
582
+ location=ctx.current_path,
583
+ )
584
+ elif key == "minimum":
585
+ next = value - 1
586
+ if seen.insert(next):
587
+ yield NegativeValue(
588
+ next,
589
+ scenario=CoverageScenario.VALUE_BELOW_MINIMUM,
590
+ description="Value smaller than minimum",
591
+ location=ctx.current_path,
592
+ )
593
+ elif key == "exclusiveMaximum" or key == "exclusiveMinimum" and seen.insert(value):
594
+ verb = "greater" if key == "exclusiveMaximum" else "smaller"
595
+ limit = "maximum" if key == "exclusiveMaximum" else "minimum"
596
+ scenario = (
597
+ CoverageScenario.VALUE_ABOVE_MAXIMUM
598
+ if key == "exclusiveMaximum"
599
+ else CoverageScenario.VALUE_BELOW_MINIMUM
600
+ )
601
+ yield NegativeValue(
602
+ value, scenario=scenario, description=f"Value {verb} than {limit}", location=ctx.current_path
603
+ )
604
+ elif key == "multipleOf":
605
+ for value_ in _negative_multiple_of(ctx, schema, value):
606
+ if seen.insert(value_.value):
607
+ yield value_
608
+ elif key == "minLength" and 0 < value < INTERNAL_BUFFER_SIZE:
609
+ if value == 1:
610
+ # In this case, the only possible negative string is an empty one
611
+ # The `pattern` value may require an non-empty one and the generation will fail
612
+ # However, it is fine to violate `pattern` here as it is negative string generation anyway
613
+ value = ""
614
+ if ctx.is_valid_for_location(value) and seen.insert(value):
615
+ yield NegativeValue(
616
+ value,
617
+ scenario=CoverageScenario.STRING_BELOW_MIN_LENGTH,
618
+ description="String smaller than minLength",
619
+ location=ctx.current_path,
620
+ )
621
+ else:
622
+ with suppress(InvalidArgument):
623
+ min_length = max_length = value - 1
624
+ new_schema = {**schema, "minLength": min_length, "maxLength": max_length}
625
+ new_schema.setdefault("type", "string")
626
+ if "pattern" in new_schema:
627
+ new_schema["pattern"] = update_quantifier(schema["pattern"], min_length, max_length)
628
+ if new_schema["pattern"] == schema["pattern"]:
629
+ # Pattern wasn't updated, try to generate a valid value then shrink the string to the required length
630
+ del new_schema["minLength"]
631
+ del new_schema["maxLength"]
632
+ value = ctx.generate_from_schema(new_schema)[:max_length]
633
+ else:
634
+ value = ctx.generate_from_schema(new_schema)
635
+ else:
636
+ value = ctx.generate_from_schema(new_schema)
637
+ if ctx.is_valid_for_location(value) and seen.insert(value):
638
+ yield NegativeValue(
639
+ value,
640
+ scenario=CoverageScenario.STRING_BELOW_MIN_LENGTH,
641
+ description="String smaller than minLength",
642
+ location=ctx.current_path,
643
+ )
644
+ elif key == "maxLength" and value < INTERNAL_BUFFER_SIZE:
645
+ try:
646
+ min_length = max_length = value + 1
647
+ new_schema = {**schema, "minLength": min_length, "maxLength": max_length}
648
+ new_schema.setdefault("type", "string")
649
+ if "pattern" in new_schema:
650
+ if value > NEGATIVE_MODE_MAX_LENGTH_WITH_PATTERN:
651
+ # Large `maxLength` value can be extremely slow to generate when combined with `pattern`
652
+ del new_schema["pattern"]
653
+ value = ctx.generate_from_schema(new_schema)
654
+ else:
655
+ new_schema["pattern"] = update_quantifier(schema["pattern"], min_length, max_length)
656
+ if new_schema["pattern"] == schema["pattern"]:
657
+ # Pattern wasn't updated, try to generate a valid value then extend the string to the required length
658
+ del new_schema["minLength"]
659
+ del new_schema["maxLength"]
660
+ value = ctx.generate_from_schema(new_schema).ljust(max_length, "0")
661
+ else:
662
+ value = ctx.generate_from_schema(new_schema)
663
+ else:
664
+ value = ctx.generate_from_schema(new_schema)
665
+ if seen.insert(value):
666
+ yield NegativeValue(
667
+ value,
668
+ scenario=CoverageScenario.STRING_ABOVE_MAX_LENGTH,
669
+ description="String larger than maxLength",
670
+ location=ctx.current_path,
671
+ )
672
+ except (InvalidArgument, Unsatisfiable):
673
+ pass
674
+ elif key == "uniqueItems" and value:
675
+ yield from _negative_unique_items(ctx, schema)
676
+ elif key == "required":
677
+ template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
678
+ yield from _negative_required(ctx, template, value)
679
+ elif key == "maxItems" and isinstance(value, int) and value < INTERNAL_BUFFER_SIZE:
680
+ if value > NEGATIVE_MODE_MAX_ITEMS:
681
+ # It could be extremely slow to generate large arrays
682
+ # Generate values up to the limit and reuse them to construct the final array
683
+ new_schema = {
684
+ **schema,
685
+ "minItems": NEGATIVE_MODE_MAX_ITEMS,
686
+ "maxItems": NEGATIVE_MODE_MAX_ITEMS,
687
+ "type": "array",
688
+ }
689
+ if "items" in schema and isinstance(schema["items"], dict):
690
+ # The schema may have another large array nested, therefore generate covering cases
691
+ # and use them to build an array for the current schema
692
+ negative = [case.value for case in cover_schema_iter(ctx, schema["items"])]
693
+ positive = [case.value for case in cover_schema_iter(ctx.with_positive(), schema["items"])]
694
+ # Interleave positive & negative values
695
+ array_value = [value for pair in zip(positive, negative) for value in pair][
696
+ :NEGATIVE_MODE_MAX_ITEMS
697
+ ]
698
+ else:
699
+ array_value = ctx.generate_from_schema(new_schema)
700
+
701
+ # Extend the array to be of length value + 1 by repeating its own elements
702
+ diff = value + 1 - len(array_value)
703
+ if diff > 0 and array_value:
704
+ array_value += (
705
+ array_value * (diff // len(array_value)) + array_value[: diff % len(array_value)]
706
+ )
707
+ if seen.insert(array_value):
708
+ yield NegativeValue(
709
+ array_value,
710
+ scenario=CoverageScenario.ARRAY_ABOVE_MAX_ITEMS,
711
+ description="Array with more items than allowed by maxItems",
712
+ location=ctx.current_path,
713
+ )
714
+ else:
715
+ try:
716
+ # Force the array to have one more item than allowed
717
+ new_schema = {**schema, "minItems": value + 1, "maxItems": value + 1, "type": "array"}
718
+ array_value = ctx.generate_from_schema(new_schema)
719
+ if seen.insert(array_value):
720
+ yield NegativeValue(
721
+ array_value,
722
+ scenario=CoverageScenario.ARRAY_ABOVE_MAX_ITEMS,
723
+ description="Array with more items than allowed by maxItems",
724
+ location=ctx.current_path,
725
+ )
726
+ except (InvalidArgument, Unsatisfiable):
727
+ pass
728
+ elif key == "minItems" and isinstance(value, int) and value > 0:
729
+ try:
730
+ # Force the array to have one less item than the minimum
731
+ new_schema = {**schema, "minItems": value - 1, "maxItems": value - 1, "type": "array"}
732
+ array_value = ctx.generate_from_schema(new_schema)
733
+ if seen.insert(array_value):
734
+ yield NegativeValue(
735
+ array_value,
736
+ scenario=CoverageScenario.ARRAY_BELOW_MIN_ITEMS,
737
+ description="Array with fewer items than allowed by minItems",
738
+ location=ctx.current_path,
739
+ )
740
+ except (InvalidArgument, Unsatisfiable):
741
+ pass
742
+ elif (
743
+ key == "additionalProperties"
744
+ and not value
745
+ and "pattern" not in schema
746
+ and schema.get("type") in ["object", None]
747
+ ):
748
+ if not ctx.allow_extra_parameters and ctx.location in (
749
+ ParameterLocation.QUERY,
750
+ ParameterLocation.HEADER,
751
+ ParameterLocation.COOKIE,
752
+ ):
753
+ continue
754
+ template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
755
+ yield NegativeValue(
756
+ {**template, UNKNOWN_PROPERTY_KEY: UNKNOWN_PROPERTY_VALUE},
757
+ scenario=CoverageScenario.OBJECT_UNEXPECTED_PROPERTIES,
758
+ description="Object with unexpected properties",
759
+ location=ctx.current_path,
760
+ )
761
+ elif key == "allOf":
762
+ nctx = ctx.with_negative()
763
+ if len(value) == 1:
764
+ with nctx.at(0):
765
+ yield from cover_schema_iter(nctx, value[0], seen)
766
+ else:
767
+ with _ignore_unfixable():
768
+ canonical = canonicalish(schema)
769
+ yield from cover_schema_iter(nctx, canonical, seen)
770
+ elif key == "anyOf":
771
+ nctx = ctx.with_negative()
772
+ validators = [jsonschema.validators.validator_for(sub_schema)(sub_schema) for sub_schema in value]
773
+ for idx, sub_schema in enumerate(value):
774
+ with nctx.at(idx):
775
+ for value in cover_schema_iter(nctx, sub_schema, seen):
776
+ # Negative value for this schema could be a positive value for another one
777
+ if is_valid_for_others(value.value, idx, validators):
778
+ continue
779
+ yield value
780
+ elif key == "oneOf":
781
+ nctx = ctx.with_negative()
782
+ validators = [jsonschema.validators.validator_for(sub_schema)(sub_schema) for sub_schema in value]
783
+ for idx, sub_schema in enumerate(value):
784
+ with nctx.at(idx):
785
+ for value in cover_schema_iter(nctx, sub_schema, seen):
786
+ if is_invalid_for_oneOf(value.value, idx, validators):
787
+ yield value
788
+ elif key == "not" and isinstance(value, (dict, bool)):
789
+ # For 'not' schemas: generate positive cases of inner schema (valid values)
790
+ # These valid values are negative for the outer schema, so flip the mode
791
+ pctx = ctx.with_positive()
792
+ yield from _flip_generation_mode_for_not(cover_schema_iter(pctx, value, seen))
793
+
794
+
795
+ def is_valid_for_others(value: Any, idx: int, validators: list[jsonschema.Validator]) -> bool:
796
+ for vidx, validator in enumerate(validators):
797
+ if idx == vidx:
798
+ # This one is being negated
799
+ continue
800
+ if validator.is_valid(value):
801
+ return True
802
+ return False
803
+
804
+
805
+ def is_invalid_for_oneOf(value: Any, idx: int, validators: list[jsonschema.Validator]) -> bool:
806
+ valid_count = 0
807
+ for vidx, validator in enumerate(validators):
808
+ if idx == vidx:
809
+ # This one is being negated
810
+ continue
811
+ if validator.is_valid(value):
812
+ valid_count += 1
813
+ # Should circuit - no need to validate more, it is already invalid
814
+ if valid_count > 1:
815
+ return True
816
+ # No matching at all - we successfully generated invalid value
817
+ return valid_count == 0
818
+
819
+
820
+ def _get_properties(schema: dict | bool) -> dict | bool:
821
+ if isinstance(schema, dict):
822
+ if "example" in schema:
823
+ return {"const": schema["example"]}
824
+ if "default" in schema:
825
+ return {"const": schema["default"]}
826
+ if schema.get("examples"):
827
+ return {"enum": schema["examples"]}
828
+ if schema.get("type") == "object":
829
+ return _get_template_schema(schema, "object")
830
+ _schema = deepclone(schema)
831
+ update_pattern_in_schema(_schema)
832
+ return _schema
833
+ return schema
834
+
835
+
836
+ def _get_template_schema(schema: dict, ty: str) -> dict:
837
+ if ty == "object":
838
+ properties = schema.get("properties")
839
+ if properties is not None:
840
+ return {
841
+ **schema,
842
+ "required": list(properties),
843
+ "type": ty,
844
+ "properties": {k: _get_properties(v) for k, v in properties.items()},
845
+ }
846
+ return {**schema, "type": ty}
847
+
848
+
849
+ def _ensure_valid_path_parameter_schema(schema: dict[str, Any]) -> dict[str, Any]:
850
+ # Path parameters should have at least 1 character length and don't contain any characters with special treatment
851
+ # on the transport level.
852
+ # The implementation below sneaks into `not` to avoid clashing with existing `pattern` keyword
853
+ not_ = schema.get("not", {}).copy()
854
+ not_["pattern"] = r"[/{}]"
855
+ return {**schema, "minLength": 1, "not": not_}
856
+
857
+
858
+ def _ensure_valid_headers_schema(schema: dict[str, Any]) -> dict[str, Any]:
859
+ # Reject any character that is not A-Z, a-z, or 0-9 for simplicity
860
+ not_ = schema.get("not", {}).copy()
861
+ not_["pattern"] = r"[^A-Za-z0-9]"
862
+ return {**schema, "not": not_}
863
+
864
+
865
+ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
866
+ """Generate positive string values."""
867
+ # Boundary and near boundary values
868
+ schema = {"type": "string", **schema}
869
+ min_length = schema.get("minLength")
870
+ if min_length == 0:
871
+ min_length = None
872
+ max_length = schema.get("maxLength")
873
+ if ctx.location == "path":
874
+ schema = _ensure_valid_path_parameter_schema(schema)
875
+ elif ctx.location in ("header", "cookie") and not ("format" in schema and schema["format"] in FORMAT_STRATEGIES):
876
+ # Don't apply it for known formats - they will insure the correct format during generation
877
+ schema = _ensure_valid_headers_schema(schema)
878
+
879
+ example = schema.get("example")
880
+ examples = schema.get("examples")
881
+ default = schema.get("default")
882
+
883
+ # Two-layer check to avoid potentially expensive data generation using schema constraints as a key
884
+ seen_values = HashSet()
885
+ seen_constraints: set[tuple] = set()
886
+
887
+ if example or examples or default:
888
+ has_valid_example = False
889
+ if example and ctx.is_valid_for_location(example) and seen_values.insert(example):
890
+ has_valid_example = True
891
+ yield PositiveValue(example, scenario=CoverageScenario.EXAMPLE_VALUE, description="Example value")
892
+ if examples:
893
+ for example in examples:
894
+ if ctx.is_valid_for_location(example) and seen_values.insert(example):
895
+ has_valid_example = True
896
+ yield PositiveValue(example, scenario=CoverageScenario.EXAMPLE_VALUE, description="Example value")
897
+ if (
898
+ default
899
+ and not (example is not None and default == example)
900
+ and not (examples is not None and any(default == ex for ex in examples))
901
+ and ctx.is_valid_for_location(default)
902
+ and seen_values.insert(default)
903
+ ):
904
+ has_valid_example = True
905
+ yield PositiveValue(default, scenario=CoverageScenario.DEFAULT_VALUE, description="Default value")
906
+ if not has_valid_example:
907
+ if not min_length and not max_length or "pattern" in schema:
908
+ value = ctx.generate_from_schema(schema)
909
+ seen_values.insert(value)
910
+ seen_constraints.add((min_length, max_length))
911
+ yield PositiveValue(value, scenario=CoverageScenario.VALID_STRING, description="Valid string")
912
+ elif not min_length and not max_length or "pattern" in schema:
913
+ value = ctx.generate_from_schema(schema)
914
+ seen_values.insert(value)
915
+ seen_constraints.add((min_length, max_length))
916
+ yield PositiveValue(value, scenario=CoverageScenario.VALID_STRING, description="Valid string")
917
+
918
+ if min_length is not None and min_length < INTERNAL_BUFFER_SIZE:
919
+ # Exactly the minimum length
920
+ key = (min_length, min_length)
921
+ if key not in seen_constraints:
922
+ seen_constraints.add(key)
923
+ value = ctx.generate_from_schema({**schema, "maxLength": min_length})
924
+ if seen_values.insert(value):
925
+ yield PositiveValue(
926
+ value, scenario=CoverageScenario.MINIMUM_LENGTH_STRING, description="Minimum length string"
927
+ )
928
+
929
+ # One character more than minimum if possible
930
+ larger = min_length + 1
931
+ key = (larger, larger)
932
+ if larger < INTERNAL_BUFFER_SIZE and key not in seen_constraints and (not max_length or larger <= max_length):
933
+ seen_constraints.add(key)
934
+ value = ctx.generate_from_schema({**schema, "minLength": larger, "maxLength": larger})
935
+ if seen_values.insert(value):
936
+ yield PositiveValue(
937
+ value,
938
+ scenario=CoverageScenario.NEAR_BOUNDARY_LENGTH_STRING,
939
+ description="Near-boundary length string",
940
+ )
941
+
942
+ if max_length is not None:
943
+ # Exactly the maximum length
944
+ key = (max_length, max_length)
945
+ if max_length < INTERNAL_BUFFER_SIZE and key not in seen_constraints:
946
+ seen_constraints.add(key)
947
+ value = ctx.generate_from_schema({**schema, "minLength": max_length, "maxLength": max_length})
948
+ if seen_values.insert(value):
949
+ yield PositiveValue(
950
+ value, scenario=CoverageScenario.MAXIMUM_LENGTH_STRING, description="Maximum length string"
951
+ )
952
+
953
+ # One character less than maximum if possible
954
+ smaller = max_length - 1
955
+ key = (smaller, smaller)
956
+ if (
957
+ smaller < INTERNAL_BUFFER_SIZE
958
+ and key not in seen_constraints
959
+ and (smaller > 0 and (min_length is None or smaller >= min_length))
960
+ ):
961
+ seen_constraints.add(key)
962
+ value = ctx.generate_from_schema({**schema, "minLength": smaller, "maxLength": smaller})
963
+ if seen_values.insert(value):
964
+ yield PositiveValue(
965
+ value,
966
+ scenario=CoverageScenario.NEAR_BOUNDARY_LENGTH_STRING,
967
+ description="Near-boundary length string",
968
+ )
969
+
970
+
971
+ def closest_multiple_greater_than(y: int, x: int) -> int:
972
+ """Find the closest multiple of X that is greater than Y."""
973
+ quotient, remainder = divmod(y, x)
974
+ if remainder == 0:
975
+ return y
976
+ return x * (quotient + 1)
977
+
978
+
979
+ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
980
+ """Generate positive integer values."""
981
+ # Boundary and near boundary values
982
+ schema = {"type": "number", **schema}
983
+ minimum = schema.get("minimum")
984
+ maximum = schema.get("maximum")
985
+ exclusive_minimum = schema.get("exclusiveMinimum")
986
+ exclusive_maximum = schema.get("exclusiveMaximum")
987
+ if exclusive_minimum is not None:
988
+ minimum = exclusive_minimum + 1
989
+ if exclusive_maximum is not None:
990
+ maximum = exclusive_maximum - 1
991
+ multiple_of = schema.get("multipleOf")
992
+ example = schema.get("example")
993
+ examples = schema.get("examples")
994
+ default = schema.get("default")
995
+
996
+ seen = HashSet()
997
+
998
+ if example or examples or default:
999
+ if example and seen.insert(example):
1000
+ yield PositiveValue(example, scenario=CoverageScenario.EXAMPLE_VALUE, description="Example value")
1001
+ if examples:
1002
+ for example in examples:
1003
+ if seen.insert(example):
1004
+ yield PositiveValue(example, scenario=CoverageScenario.EXAMPLE_VALUE, description="Example value")
1005
+ if (
1006
+ default
1007
+ and not (example is not None and default == example)
1008
+ and not (examples is not None and any(default == ex for ex in examples))
1009
+ and seen.insert(default)
1010
+ ):
1011
+ yield PositiveValue(default, scenario=CoverageScenario.DEFAULT_VALUE, description="Default value")
1012
+ elif not minimum and not maximum:
1013
+ value = ctx.generate_from_schema(schema)
1014
+ seen.insert(value)
1015
+ yield PositiveValue(value, scenario=CoverageScenario.VALID_NUMBER, description="Valid number")
1016
+
1017
+ if minimum is not None:
1018
+ # Exactly the minimum
1019
+ if multiple_of is not None:
1020
+ smallest = closest_multiple_greater_than(minimum, multiple_of)
1021
+ else:
1022
+ smallest = minimum
1023
+ if seen.insert(smallest):
1024
+ yield PositiveValue(smallest, scenario=CoverageScenario.MINIMUM_VALUE, description="Minimum value")
1025
+
1026
+ # One more than minimum if possible
1027
+ if multiple_of is not None:
1028
+ larger = smallest + multiple_of
1029
+ else:
1030
+ larger = minimum + 1
1031
+ if (not maximum or larger <= maximum) and seen.insert(larger):
1032
+ yield PositiveValue(
1033
+ larger, scenario=CoverageScenario.NEAR_BOUNDARY_NUMBER, description="Near-boundary number"
1034
+ )
1035
+
1036
+ if maximum is not None:
1037
+ # Exactly the maximum
1038
+ if multiple_of is not None:
1039
+ largest = maximum - (maximum % multiple_of)
1040
+ else:
1041
+ largest = maximum
1042
+ if seen.insert(largest):
1043
+ yield PositiveValue(largest, scenario=CoverageScenario.MAXIMUM_VALUE, description="Maximum value")
1044
+
1045
+ # One less than maximum if possible
1046
+ if multiple_of is not None:
1047
+ smaller = largest - multiple_of
1048
+ else:
1049
+ smaller = maximum - 1
1050
+ if (minimum is None or smaller >= minimum) and seen.insert(smaller):
1051
+ yield PositiveValue(
1052
+ smaller, scenario=CoverageScenario.NEAR_BOUNDARY_NUMBER, description="Near-boundary number"
1053
+ )
1054
+
1055
+
1056
+ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Generator[GeneratedValue, None, None]:
1057
+ example = schema.get("example")
1058
+ examples = schema.get("examples")
1059
+ default = schema.get("default")
1060
+
1061
+ seen = HashSet()
1062
+ seen_constraints: set[tuple] = set()
1063
+
1064
+ if example or examples or default:
1065
+ if example and seen.insert(example):
1066
+ yield PositiveValue(example, scenario=CoverageScenario.EXAMPLE_VALUE, description="Example value")
1067
+ if examples:
1068
+ for example in examples:
1069
+ if seen.insert(example):
1070
+ yield PositiveValue(example, scenario=CoverageScenario.EXAMPLE_VALUE, description="Example value")
1071
+ if (
1072
+ default
1073
+ and not (example is not None and default == example)
1074
+ and not (examples is not None and any(default == ex for ex in examples))
1075
+ and seen.insert(default)
1076
+ ):
1077
+ yield PositiveValue(default, scenario=CoverageScenario.DEFAULT_VALUE, description="Default value")
1078
+ elif seen.insert(template):
1079
+ yield PositiveValue(template, scenario=CoverageScenario.VALID_ARRAY, description="Valid array")
1080
+
1081
+ # Boundary and near-boundary sizes
1082
+ min_items = schema.get("minItems")
1083
+ max_items = schema.get("maxItems")
1084
+ if min_items is not None:
1085
+ # Do not generate an array with `minItems` length, because it is already covered by `template`
1086
+ # One item more than minimum if possible
1087
+ larger = min_items + 1
1088
+ if (max_items is None or larger <= max_items) and larger not in seen_constraints:
1089
+ seen_constraints.add(larger)
1090
+ value = ctx.generate_from_schema({**schema, "minItems": larger, "maxItems": larger})
1091
+ if seen.insert(value):
1092
+ yield PositiveValue(
1093
+ value, scenario=CoverageScenario.NEAR_BOUNDARY_ITEMS_ARRAY, description="Near-boundary items array"
1094
+ )
1095
+
1096
+ if max_items is not None:
1097
+ if max_items < INTERNAL_BUFFER_SIZE and max_items not in seen_constraints:
1098
+ seen_constraints.add(max_items)
1099
+ value = ctx.generate_from_schema({**schema, "minItems": max_items})
1100
+ if seen.insert(value):
1101
+ yield PositiveValue(
1102
+ value, scenario=CoverageScenario.MAXIMUM_ITEMS_ARRAY, description="Maximum items array"
1103
+ )
1104
+
1105
+ # One item smaller than maximum if possible
1106
+ smaller = max_items - 1
1107
+ if (
1108
+ smaller < INTERNAL_BUFFER_SIZE
1109
+ and smaller > 0
1110
+ and (min_items is None or smaller >= min_items)
1111
+ and smaller not in seen_constraints
1112
+ ):
1113
+ value = ctx.generate_from_schema({**schema, "minItems": smaller, "maxItems": smaller})
1114
+ if seen.insert(value):
1115
+ yield PositiveValue(
1116
+ value, scenario=CoverageScenario.NEAR_BOUNDARY_ITEMS_ARRAY, description="Near-boundary items array"
1117
+ )
1118
+
1119
+ if "items" in schema and "enum" in schema["items"] and isinstance(schema["items"]["enum"], list) and max_items != 0:
1120
+ # Ensure there is enough items to pass `minItems` if it is specified
1121
+ length = min_items or 1
1122
+ for variant in schema["items"]["enum"]:
1123
+ value = [variant] * length
1124
+ if seen.insert(value):
1125
+ yield PositiveValue(
1126
+ value,
1127
+ scenario=CoverageScenario.ENUM_VALUE_ITEMS_ARRAY,
1128
+ description="Enum value from available for items array",
1129
+ )
1130
+ elif min_items is None and max_items is None and "items" in schema and isinstance(schema["items"], dict):
1131
+ # Otherwise only an empty array is generated
1132
+ sub_schema = schema["items"]
1133
+ for item in cover_schema_iter(ctx, sub_schema):
1134
+ yield PositiveValue(
1135
+ [item.value],
1136
+ scenario=CoverageScenario.VALID_ARRAY,
1137
+ description=f"Single-item array: {item.description}",
1138
+ )
1139
+
1140
+
1141
+ def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Generator[GeneratedValue, None, None]:
1142
+ example = schema.get("example")
1143
+ examples = schema.get("examples")
1144
+ default = schema.get("default")
1145
+
1146
+ if example or examples or default:
1147
+ if example:
1148
+ yield PositiveValue(example, scenario=CoverageScenario.EXAMPLE_VALUE, description="Example value")
1149
+ if examples:
1150
+ for example in examples:
1151
+ yield PositiveValue(example, scenario=CoverageScenario.EXAMPLE_VALUE, description="Example value")
1152
+ if (
1153
+ default
1154
+ and not (example is not None and default == example)
1155
+ and not (examples is not None and any(default == ex for ex in examples))
1156
+ ):
1157
+ yield PositiveValue(default, scenario=CoverageScenario.DEFAULT_VALUE, description="Default value")
1158
+ else:
1159
+ yield PositiveValue(template, scenario=CoverageScenario.VALID_OBJECT, description="Valid object")
1160
+
1161
+ properties = schema.get("properties", {})
1162
+ required = set(schema.get("required", []))
1163
+ optional = list(set(properties) - required)
1164
+ optional.sort()
1165
+
1166
+ # Generate combinations with required properties and one optional property
1167
+ for name in optional:
1168
+ combo = {k: v for k, v in template.items() if k in required or k == name}
1169
+ if combo != template:
1170
+ yield PositiveValue(
1171
+ combo,
1172
+ scenario=CoverageScenario.OBJECT_REQUIRED_AND_OPTIONAL,
1173
+ description=f"Object with all required properties and '{name}'",
1174
+ )
1175
+ # Generate one combination for each size from 2 to N-1
1176
+ for selection in select_combinations(optional):
1177
+ combo = {k: v for k, v in template.items() if k in required or k in selection}
1178
+ yield PositiveValue(
1179
+ combo,
1180
+ scenario=CoverageScenario.OBJECT_REQUIRED_AND_OPTIONAL,
1181
+ description="Object with all required and a subset of optional properties",
1182
+ )
1183
+ # Generate only required properties
1184
+ if set(properties) != required:
1185
+ only_required = {k: v for k, v in template.items() if k in required}
1186
+ yield PositiveValue(
1187
+ only_required,
1188
+ scenario=CoverageScenario.OBJECT_ONLY_REQUIRED,
1189
+ description="Object with only required properties",
1190
+ )
1191
+ seen = HashSet()
1192
+ for name, sub_schema in properties.items():
1193
+ seen.insert(template.get(name))
1194
+ for new in cover_schema_iter(ctx, sub_schema):
1195
+ if seen.insert(new.value):
1196
+ yield PositiveValue(
1197
+ {**template, name: new.value},
1198
+ scenario=CoverageScenario.VALID_OBJECT,
1199
+ description=f"Object with valid '{name}' value: {new.description}",
1200
+ )
1201
+ seen.clear()
1202
+
1203
+
1204
+ def select_combinations(optional: list[str]) -> Iterator[tuple[str, ...]]:
1205
+ for size in range(2, len(optional)):
1206
+ yield next(combinations(optional, size))
1207
+
1208
+
1209
+ def _negative_enum(ctx: CoverageContext, value: list, seen: HashSet) -> Generator[GeneratedValue, None, None]:
1210
+ def is_not_in_value(x: Any) -> bool:
1211
+ if x in value or not ctx.is_valid_for_location(x):
1212
+ return False
1213
+ return seen.insert(x)
1214
+
1215
+ strategy = (
1216
+ st.text(alphabet=st.characters(min_codepoint=65, max_codepoint=122, categories=["L"]), min_size=3)
1217
+ | st.none()
1218
+ | st.booleans()
1219
+ | NUMERIC_STRATEGY
1220
+ ).filter(is_not_in_value)
1221
+ yield NegativeValue(
1222
+ ctx.generate_from(strategy),
1223
+ scenario=CoverageScenario.INVALID_ENUM_VALUE,
1224
+ description="Invalid enum value",
1225
+ location=ctx.current_path,
1226
+ )
1227
+
1228
+
1229
+ def _negative_properties(
1230
+ ctx: CoverageContext, template: dict, properties: dict
1231
+ ) -> Generator[GeneratedValue, None, None]:
1232
+ nctx = ctx.with_negative()
1233
+ for key, sub_schema in properties.items():
1234
+ with nctx.at(key):
1235
+ for value in cover_schema_iter(nctx, sub_schema):
1236
+ yield NegativeValue(
1237
+ {**template, key: value.value},
1238
+ scenario=value.scenario,
1239
+ description=f"Object with invalid '{key}' value: {value.description}",
1240
+ location=nctx.current_path,
1241
+ parameter=key,
1242
+ )
1243
+
1244
+
1245
+ def _negative_pattern_properties(
1246
+ ctx: CoverageContext, template: dict, pattern_properties: dict
1247
+ ) -> Generator[GeneratedValue, None, None]:
1248
+ nctx = ctx.with_negative()
1249
+ for pattern, sub_schema in pattern_properties.items():
1250
+ try:
1251
+ key = ctx.generate_from(st.from_regex(pattern))
1252
+ except re.error:
1253
+ continue
1254
+ with nctx.at(pattern):
1255
+ for value in cover_schema_iter(nctx, sub_schema):
1256
+ yield NegativeValue(
1257
+ {**template, key: value.value},
1258
+ scenario=value.scenario,
1259
+ description=f"Object with invalid pattern key '{key}' ('{pattern}') value: {value.description}",
1260
+ location=nctx.current_path,
1261
+ )
1262
+
1263
+
1264
+ def _negative_items(ctx: CoverageContext, schema: dict[str, Any] | bool) -> Generator[GeneratedValue, None, None]:
1265
+ """Arrays not matching the schema."""
1266
+ nctx = ctx.with_negative()
1267
+ for value in cover_schema_iter(nctx, schema):
1268
+ items = [value.value]
1269
+ if ctx.leads_to_negative_test_case(items):
1270
+ yield NegativeValue(
1271
+ items,
1272
+ scenario=value.scenario,
1273
+ description=f"Array with invalid items: {value.description}",
1274
+ location=nctx.current_path,
1275
+ )
1276
+
1277
+
1278
+ def _not_matching_pattern(value: str, pattern: re.Pattern) -> bool:
1279
+ return pattern.search(value) is None
1280
+
1281
+
1282
+ def _negative_pattern(
1283
+ ctx: CoverageContext, pattern: str, min_length: int | None = None, max_length: int | None = None
1284
+ ) -> Generator[GeneratedValue, None, None]:
1285
+ try:
1286
+ compiled = re.compile(pattern)
1287
+ except re.error:
1288
+ return
1289
+ yield NegativeValue(
1290
+ ctx.generate_from(
1291
+ st.text(min_size=min_length or 0, max_size=max_length)
1292
+ .filter(partial(_not_matching_pattern, pattern=compiled))
1293
+ .filter(ctx.is_valid_for_location)
1294
+ ),
1295
+ scenario=CoverageScenario.INVALID_PATTERN,
1296
+ description=f"Value not matching the '{pattern}' pattern",
1297
+ location=ctx.current_path,
1298
+ )
1299
+
1300
+
1301
+ def _with_negated_key(schema: dict, key: str, value: Any) -> dict:
1302
+ return {"allOf": [{k: v for k, v in schema.items() if k != key}, {"not": {key: value}}]}
1303
+
1304
+
1305
+ def _negative_multiple_of(
1306
+ ctx: CoverageContext, schema: dict, multiple_of: int | float
1307
+ ) -> Generator[GeneratedValue, None, None]:
1308
+ yield NegativeValue(
1309
+ ctx.generate_from_schema(_with_negated_key(schema, "multipleOf", multiple_of)),
1310
+ scenario=CoverageScenario.NOT_MULTIPLE_OF,
1311
+ description=f"Non-multiple of {multiple_of}",
1312
+ location=ctx.current_path,
1313
+ )
1314
+
1315
+
1316
+ def _negative_unique_items(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
1317
+ unique = jsonify(ctx.generate_from_schema({**schema, "type": "array", "minItems": 1, "maxItems": 1}))
1318
+ yield NegativeValue(
1319
+ unique + unique,
1320
+ scenario=CoverageScenario.NON_UNIQUE_ITEMS,
1321
+ description="Non-unique items",
1322
+ location=ctx.current_path,
1323
+ )
1324
+
1325
+
1326
+ def _negative_required(
1327
+ ctx: CoverageContext, template: dict, required: list[str]
1328
+ ) -> Generator[GeneratedValue, None, None]:
1329
+ for key in required:
1330
+ yield NegativeValue(
1331
+ {k: v for k, v in template.items() if k != key},
1332
+ scenario=CoverageScenario.OBJECT_MISSING_REQUIRED_PROPERTY,
1333
+ description=f"Missing required property: {key}",
1334
+ location=ctx.current_path,
1335
+ parameter=key,
1336
+ )
1337
+
1338
+
1339
+ def _is_invalid_hostname(v: Any) -> bool:
1340
+ return v == "" or not jsonschema.Draft202012Validator.FORMAT_CHECKER.conforms(v, "hostname")
1341
+
1342
+
1343
+ def _is_invalid_format(v: Any, format: str) -> bool:
1344
+ return not jsonschema.Draft202012Validator.FORMAT_CHECKER.conforms(v, format)
1345
+
1346
+
1347
+ def _negative_format(ctx: CoverageContext, schema: dict, format: str) -> Generator[GeneratedValue, None, None]:
1348
+ # Hypothesis-jsonschema does not canonicalise it properly right now, which leads to unsatisfiable schema
1349
+ without_format = {k: v for k, v in schema.items() if k != "format"}
1350
+ without_format.setdefault("type", "string")
1351
+ if ctx.location == "path":
1352
+ # Empty path parameters are invalid
1353
+ without_format["minLength"] = 1
1354
+ strategy = from_schema(without_format)
1355
+ if format in jsonschema.Draft202012Validator.FORMAT_CHECKER.checkers:
1356
+ if format == "hostname":
1357
+ strategy = strategy.filter(_is_invalid_hostname)
1358
+ else:
1359
+ strategy = strategy.filter(functools.partial(_is_invalid_format, format=format))
1360
+ yield NegativeValue(
1361
+ ctx.generate_from(strategy),
1362
+ scenario=CoverageScenario.INVALID_FORMAT,
1363
+ description=f"Value not matching the '{format}' format",
1364
+ location=ctx.current_path,
1365
+ )
1366
+
1367
+
1368
+ def _is_non_integer_float(x: float) -> bool:
1369
+ return x != int(x)
1370
+
1371
+
1372
+ def is_valid_header_value(value: Any) -> bool:
1373
+ value = str(value)
1374
+ if not is_latin_1_encodable(value):
1375
+ return False
1376
+ if has_invalid_characters("A", value):
1377
+ return False
1378
+ return True
1379
+
1380
+
1381
+ def jsonify(value: Any) -> Any:
1382
+ if isinstance(value, bool):
1383
+ return "true" if value else "false"
1384
+ elif value is None:
1385
+ return "null"
1386
+
1387
+ stack: list = [value]
1388
+ while stack:
1389
+ item = stack.pop()
1390
+ if isinstance(item, dict):
1391
+ for key, sub_item in item.items():
1392
+ if isinstance(sub_item, bool):
1393
+ item[key] = "true" if sub_item else "false"
1394
+ elif sub_item is None:
1395
+ item[key] = "null"
1396
+ elif isinstance(sub_item, dict):
1397
+ stack.append(sub_item)
1398
+ elif isinstance(sub_item, list):
1399
+ stack.extend(item)
1400
+ elif isinstance(item, list):
1401
+ for idx, sub_item in enumerate(item):
1402
+ if isinstance(sub_item, bool):
1403
+ item[idx] = "true" if sub_item else "false"
1404
+ elif sub_item is None:
1405
+ item[idx] = "null"
1406
+ else:
1407
+ stack.extend(item)
1408
+ return value
1409
+
1410
+
1411
+ def quote_path_parameter(value: Any) -> str:
1412
+ if isinstance(value, str):
1413
+ if value == ".":
1414
+ return "%2E"
1415
+ elif value == "..":
1416
+ return "%2E%2E"
1417
+ else:
1418
+ return quote_plus(value)
1419
+ if isinstance(value, list):
1420
+ return ",".join(map(str, value))
1421
+ return str(value)
1422
+
1423
+
1424
+ def _negative_type(
1425
+ ctx: CoverageContext, ty: str | list[str], seen: HashSet, schema: dict[str, Any]
1426
+ ) -> Generator[GeneratedValue, None, None]:
1427
+ if isinstance(ty, str):
1428
+ types = [ty]
1429
+ else:
1430
+ types = ty
1431
+ strategies = {ty: strategy for ty, strategy in STRATEGIES_FOR_TYPE.items() if ty not in types}
1432
+
1433
+ filter_func = {
1434
+ "path": lambda x: not is_invalid_path_parameter(x),
1435
+ "header": is_valid_header_value,
1436
+ "cookie": is_valid_header_value,
1437
+ "query": lambda x: not contains_unicode_surrogate_pair(x),
1438
+ }.get(ctx.location)
1439
+
1440
+ if "number" in types:
1441
+ strategies.pop("integer", None)
1442
+ if "integer" in types:
1443
+ strategies["number"] = FLOAT_STRATEGY.filter(_is_non_integer_float)
1444
+ if ctx.location == ParameterLocation.QUERY:
1445
+ strategies.pop("object", None)
1446
+ if filter_func is not None:
1447
+ for ty, strategy in strategies.items():
1448
+ strategies[ty] = strategy.filter(filter_func)
1449
+
1450
+ pattern = schema.get("pattern")
1451
+ if pattern is not None:
1452
+ try:
1453
+ re.compile(pattern)
1454
+ except re.error:
1455
+ schema = schema.copy()
1456
+ del schema["pattern"]
1457
+ return
1458
+
1459
+ validator = ctx.validator_cls(
1460
+ schema,
1461
+ format_checker=jsonschema.Draft202012Validator.FORMAT_CHECKER,
1462
+ )
1463
+ is_valid = validator.is_valid
1464
+ try:
1465
+ is_valid(None)
1466
+ apply_validation = True
1467
+ except Exception:
1468
+ # Schema is not correct and we can't validate the generated instances.
1469
+ # In such a scenario it is better to generate at least something with some chances to have a false
1470
+ # positive failure
1471
+ apply_validation = False
1472
+
1473
+ def _does_not_match_the_original_schema(value: Any) -> bool:
1474
+ return not is_valid(str(value))
1475
+
1476
+ if ctx.location == ParameterLocation.PATH:
1477
+ for ty, strategy in strategies.items():
1478
+ strategies[ty] = strategy.map(jsonify).map(quote_path_parameter)
1479
+ elif ctx.location == ParameterLocation.QUERY:
1480
+ for ty, strategy in strategies.items():
1481
+ strategies[ty] = strategy.map(jsonify)
1482
+
1483
+ if apply_validation and ctx.will_be_serialized_to_string():
1484
+ for ty, strategy in strategies.items():
1485
+ strategies[ty] = strategy.filter(_does_not_match_the_original_schema)
1486
+ for strategy in strategies.values():
1487
+ value = ctx.generate_from(strategy)
1488
+ if seen.insert(value) and ctx.is_valid_for_location(value):
1489
+ yield NegativeValue(
1490
+ value, scenario=CoverageScenario.INCORRECT_TYPE, description="Incorrect type", location=ctx.current_path
1491
+ )
1492
+
1493
+
1494
+ def _flip_generation_mode_for_not(
1495
+ values: Generator[GeneratedValue, None, None],
1496
+ ) -> Generator[GeneratedValue, None, None]:
1497
+ """Flip generation mode for values from 'not' schemas.
1498
+
1499
+ For 'not' schemas, the semantic is inverted:
1500
+ - Positive values for the inner schema are negative for the outer schema
1501
+ - Negative values for the inner schema are positive for the outer schema
1502
+ """
1503
+ for value in values:
1504
+ flipped_mode = (
1505
+ GenerationMode.NEGATIVE if value.generation_mode == GenerationMode.POSITIVE else GenerationMode.POSITIVE
1506
+ )
1507
+ yield GeneratedValue(
1508
+ value=value.value,
1509
+ generation_mode=flipped_mode,
1510
+ scenario=value.scenario,
1511
+ description=value.description,
1512
+ location=value.location,
1513
+ parameter=value.parameter,
1514
+ )
1515
+
1516
+
1517
+ def push_examples_to_properties(schema: dict[str, Any]) -> None:
1518
+ """Push examples from the top-level 'examples' field to the corresponding properties."""
1519
+ if "examples" in schema and "properties" in schema:
1520
+ properties = schema["properties"]
1521
+ for example in schema["examples"]:
1522
+ if isinstance(example, dict):
1523
+ for prop, value in example.items():
1524
+ if prop in properties:
1525
+ if "examples" not in properties[prop]:
1526
+ properties[prop]["examples"] = []
1527
+ if value not in schema["properties"][prop]["examples"]:
1528
+ properties[prop]["examples"].append(value)