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,992 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import inspect
5
+ import warnings
6
+ from dataclasses import dataclass
7
+ from enum import Enum
8
+ from functools import wraps
9
+ from itertools import combinations
10
+ from time import perf_counter
11
+ from typing import Any, Callable, Generator, Mapping
12
+
13
+ import hypothesis
14
+ from hypothesis import Phase, Verbosity
15
+ from hypothesis import strategies as st
16
+ from hypothesis._settings import all_settings
17
+ from hypothesis.errors import Unsatisfiable
18
+ from jsonschema.exceptions import SchemaError
19
+ from requests.models import CaseInsensitiveDict
20
+
21
+ from schemathesis import auths
22
+ from schemathesis.auths import AuthStorage, AuthStorageMark
23
+ from schemathesis.config import GenerationConfig, ProjectConfig
24
+ from schemathesis.core import INJECTED_PATH_PARAMETER_KEY, NOT_SET, NotSet, SpecificationFeature, media_types
25
+ from schemathesis.core.errors import (
26
+ InfiniteRecursiveReference,
27
+ InvalidSchema,
28
+ MalformedMediaType,
29
+ SerializationNotPossible,
30
+ UnresolvableReference,
31
+ )
32
+ from schemathesis.core.marks import Mark
33
+ from schemathesis.core.parameters import LOCATION_TO_CONTAINER, ParameterLocation
34
+ from schemathesis.core.transforms import deepclone
35
+ from schemathesis.core.transport import prepare_urlencoded
36
+ from schemathesis.core.validation import has_invalid_characters, is_latin_1_encodable
37
+ from schemathesis.generation import GenerationMode, coverage
38
+ from schemathesis.generation.case import Case
39
+ from schemathesis.generation.hypothesis import examples, setup
40
+ from schemathesis.generation.hypothesis.examples import add_single_example
41
+ from schemathesis.generation.hypothesis.given import GivenInput
42
+ from schemathesis.generation.meta import (
43
+ CaseMetadata,
44
+ ComponentInfo,
45
+ CoveragePhaseData,
46
+ CoverageScenario,
47
+ GenerationInfo,
48
+ PhaseInfo,
49
+ )
50
+ from schemathesis.hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookDispatcherMark
51
+ from schemathesis.schemas import APIOperation, ParameterSet
52
+
53
+ setup()
54
+
55
+
56
+ class HypothesisTestMode(str, Enum):
57
+ EXAMPLES = "examples"
58
+ COVERAGE = "coverage"
59
+ FUZZING = "fuzzing"
60
+
61
+
62
+ @dataclass
63
+ class HypothesisTestConfig:
64
+ project: ProjectConfig
65
+ modes: list[HypothesisTestMode]
66
+ settings: hypothesis.settings | None
67
+ seed: int | None
68
+ as_strategy_kwargs: dict[str, Any]
69
+ given_args: tuple[GivenInput, ...]
70
+ given_kwargs: dict[str, GivenInput]
71
+
72
+ __slots__ = (
73
+ "project",
74
+ "modes",
75
+ "settings",
76
+ "seed",
77
+ "as_strategy_kwargs",
78
+ "given_args",
79
+ "given_kwargs",
80
+ )
81
+
82
+ def __init__(
83
+ self,
84
+ project: ProjectConfig,
85
+ modes: list[HypothesisTestMode],
86
+ settings: hypothesis.settings | None = None,
87
+ seed: int | None = None,
88
+ as_strategy_kwargs: dict[str, Any] | None = None,
89
+ given_args: tuple[GivenInput, ...] = (),
90
+ given_kwargs: dict[str, GivenInput] | None = None,
91
+ ) -> None:
92
+ self.project = project
93
+ self.modes = modes
94
+ self.settings = settings
95
+ self.seed = seed
96
+ self.as_strategy_kwargs = as_strategy_kwargs or {}
97
+ self.given_args = given_args
98
+ self.given_kwargs = given_kwargs or {}
99
+
100
+
101
+ def create_test(
102
+ *,
103
+ operation: APIOperation,
104
+ test_func: Callable,
105
+ config: HypothesisTestConfig,
106
+ ) -> Callable:
107
+ """Create a Hypothesis test."""
108
+ hook_dispatcher = HookDispatcherMark.get(test_func)
109
+ auth_storage = AuthStorageMark.get(test_func)
110
+
111
+ strategy_kwargs = {
112
+ "hooks": hook_dispatcher,
113
+ "auth_storage": auth_storage,
114
+ **config.as_strategy_kwargs,
115
+ }
116
+ generation = config.project.generation_for(operation=operation)
117
+ strategy = st.one_of(operation.as_strategy(generation_mode=mode, **strategy_kwargs) for mode in generation.modes)
118
+
119
+ hypothesis_test = create_base_test(
120
+ test_function=test_func,
121
+ strategy=strategy,
122
+ args=config.given_args,
123
+ kwargs=config.given_kwargs,
124
+ )
125
+
126
+ ApiOperationMark.set(hypothesis_test, operation)
127
+
128
+ if config.seed is not None:
129
+ hypothesis_test = hypothesis.seed(config.seed)(hypothesis_test)
130
+
131
+ default = hypothesis.settings.default
132
+ settings = getattr(hypothesis_test, SETTINGS_ATTRIBUTE_NAME, None)
133
+ assert settings is not None
134
+
135
+ if settings.verbosity == default.verbosity:
136
+ settings = hypothesis.settings(settings, verbosity=Verbosity.quiet)
137
+
138
+ if config.settings is not None:
139
+ # Merge the user-provided settings with the current ones
140
+ settings = hypothesis.settings(
141
+ config.settings,
142
+ **{
143
+ item: getattr(settings, item)
144
+ for item in all_settings
145
+ if getattr(settings, item) != getattr(default, item)
146
+ },
147
+ )
148
+
149
+ if Phase.explain in settings.phases:
150
+ phases = tuple(phase for phase in settings.phases if phase != Phase.explain)
151
+ settings = hypothesis.settings(settings, phases=phases)
152
+
153
+ # Remove `reuse` & `generate` phases to avoid yielding any test cases if we don't do fuzzing
154
+ if HypothesisTestMode.FUZZING not in config.modes and (
155
+ Phase.generate in settings.phases or Phase.reuse in settings.phases
156
+ ):
157
+ phases = tuple(phase for phase in settings.phases if phase not in (Phase.reuse, Phase.generate))
158
+ settings = hypothesis.settings(settings, phases=phases)
159
+
160
+ specification = operation.schema.specification
161
+
162
+ # Add examples if explicit phase is enabled
163
+ if (
164
+ HypothesisTestMode.EXAMPLES in config.modes
165
+ and Phase.explicit in settings.phases
166
+ and specification.supports_feature(SpecificationFeature.EXAMPLES)
167
+ ):
168
+ phases_config = config.project.phases_for(operation=operation)
169
+ hypothesis_test = add_examples(
170
+ hypothesis_test,
171
+ operation,
172
+ fill_missing=phases_config.examples.fill_missing,
173
+ hook_dispatcher=hook_dispatcher,
174
+ **strategy_kwargs,
175
+ )
176
+
177
+ if (
178
+ HypothesisTestMode.COVERAGE in config.modes
179
+ and Phase.explicit in settings.phases
180
+ and specification.supports_feature(SpecificationFeature.COVERAGE)
181
+ and not config.given_args
182
+ and not config.given_kwargs
183
+ ):
184
+ phases_config = config.project.phases_for(operation=operation)
185
+ hypothesis_test = add_coverage(
186
+ hypothesis_test,
187
+ operation,
188
+ generation.modes,
189
+ auth_storage,
190
+ config.as_strategy_kwargs,
191
+ generate_duplicate_query_parameters=phases_config.coverage.generate_duplicate_query_parameters,
192
+ unexpected_methods=phases_config.coverage.unexpected_methods,
193
+ generation_config=generation,
194
+ )
195
+
196
+ injected_path_parameter_names = [
197
+ parameter.name
198
+ for parameter in operation.path_parameters
199
+ if parameter.definition.get(INJECTED_PATH_PARAMETER_KEY)
200
+ ]
201
+ if injected_path_parameter_names:
202
+ names = ", ".join(f"'{name}'" for name in injected_path_parameter_names)
203
+ plural = "s" if len(injected_path_parameter_names) > 1 else ""
204
+ verb = "are" if len(injected_path_parameter_names) > 1 else "is"
205
+ error = InvalidSchema(f"Path parameter{plural} {names} {verb} not defined")
206
+ MissingPathParameters.set(hypothesis_test, error)
207
+
208
+ setattr(hypothesis_test, SETTINGS_ATTRIBUTE_NAME, settings)
209
+
210
+ return hypothesis_test
211
+
212
+
213
+ SETTINGS_ATTRIBUTE_NAME = "_hypothesis_internal_use_settings"
214
+
215
+
216
+ def create_base_test(
217
+ *,
218
+ test_function: Callable,
219
+ strategy: st.SearchStrategy,
220
+ args: tuple[GivenInput, ...],
221
+ kwargs: dict[str, GivenInput],
222
+ ) -> Callable:
223
+ """Create the basic Hypothesis test with the given strategy."""
224
+
225
+ @wraps(test_function)
226
+ def test_wrapper(*args: Any, **kwargs: Any) -> Any:
227
+ __tracebackhide__ = True
228
+ return test_function(*args, **kwargs)
229
+
230
+ funcobj = hypothesis.given(*args, **{**kwargs, "case": strategy})(test_wrapper)
231
+
232
+ if inspect.iscoroutinefunction(test_function):
233
+ funcobj.hypothesis.inner_test = make_async_test(test_function)
234
+ return funcobj
235
+
236
+
237
+ def make_async_test(test: Callable) -> Callable:
238
+ def async_run(*args: Any, **kwargs: Any) -> None:
239
+ try:
240
+ loop = asyncio.get_event_loop()
241
+ except RuntimeError:
242
+ loop = asyncio.new_event_loop()
243
+ coro = test(*args, **kwargs)
244
+ future = asyncio.ensure_future(coro, loop=loop)
245
+ loop.run_until_complete(future)
246
+
247
+ return async_run
248
+
249
+
250
+ def add_examples(
251
+ test: Callable,
252
+ operation: APIOperation,
253
+ fill_missing: bool,
254
+ hook_dispatcher: HookDispatcher | None = None,
255
+ **kwargs: Any,
256
+ ) -> Callable:
257
+ for example in generate_example_cases(
258
+ test=test, operation=operation, fill_missing=fill_missing, hook_dispatcher=hook_dispatcher, **kwargs
259
+ ):
260
+ test = hypothesis.example(case=example)(test)
261
+
262
+ return test
263
+
264
+
265
+ def generate_example_cases(
266
+ *,
267
+ test: Callable,
268
+ operation: APIOperation,
269
+ fill_missing: bool,
270
+ hook_dispatcher: HookDispatcher | None = None,
271
+ **kwargs: Any,
272
+ ) -> Generator[Case]:
273
+ """Add examples to the Hypothesis test, if they are specified in the schema."""
274
+ try:
275
+ result: list[Case] = [
276
+ examples.generate_one(strategy) for strategy in operation.get_strategies_from_examples(**kwargs)
277
+ ]
278
+ except (
279
+ InvalidSchema,
280
+ InfiniteRecursiveReference,
281
+ Unsatisfiable,
282
+ UnresolvableReference,
283
+ SerializationNotPossible,
284
+ SchemaError,
285
+ ) as exc:
286
+ result = []
287
+ if isinstance(exc, Unsatisfiable):
288
+ UnsatisfiableExampleMark.set(test, exc)
289
+ if isinstance(exc, SerializationNotPossible):
290
+ NonSerializableMark.set(test, exc)
291
+ if isinstance(exc, SchemaError):
292
+ InvalidRegexMark.set(test, exc)
293
+ if isinstance(exc, InfiniteRecursiveReference):
294
+ InfiniteRecursiveReferenceMark.set(test, exc)
295
+ if isinstance(exc, UnresolvableReference):
296
+ UnresolvableReferenceMark.set(test, exc)
297
+
298
+ if fill_missing and not result:
299
+ strategy = operation.as_strategy()
300
+ add_single_example(strategy, result)
301
+
302
+ context = HookContext(operation=operation) # context should be passed here instead
303
+ GLOBAL_HOOK_DISPATCHER.dispatch("before_add_examples", context, result)
304
+ operation.schema.hooks.dispatch("before_add_examples", context, result)
305
+ if hook_dispatcher:
306
+ hook_dispatcher.dispatch("before_add_examples", context, result)
307
+ original_test = test
308
+ for example in result:
309
+ if example.headers is not None:
310
+ invalid_headers = dict(find_invalid_headers(example.headers))
311
+ if invalid_headers:
312
+ InvalidHeadersExampleMark.set(original_test, invalid_headers)
313
+ continue
314
+ adjust_urlencoded_payload(example)
315
+ yield example
316
+
317
+
318
+ def adjust_urlencoded_payload(case: Case) -> None:
319
+ if case.media_type is not None:
320
+ try:
321
+ media_type = media_types.parse(case.media_type)
322
+ if media_type == ("application", "x-www-form-urlencoded"):
323
+ case.body = prepare_urlencoded(case.body)
324
+ except ValueError:
325
+ pass
326
+
327
+
328
+ def add_coverage(
329
+ test: Callable,
330
+ operation: APIOperation,
331
+ generation_modes: list[GenerationMode],
332
+ auth_storage: AuthStorage | None,
333
+ as_strategy_kwargs: dict[str, Any],
334
+ generate_duplicate_query_parameters: bool,
335
+ unexpected_methods: set[str],
336
+ generation_config: GenerationConfig,
337
+ ) -> Callable:
338
+ for case in generate_coverage_cases(
339
+ operation=operation,
340
+ generation_modes=generation_modes,
341
+ auth_storage=auth_storage,
342
+ as_strategy_kwargs=as_strategy_kwargs,
343
+ generate_duplicate_query_parameters=generate_duplicate_query_parameters,
344
+ unexpected_methods=unexpected_methods,
345
+ generation_config=generation_config,
346
+ ):
347
+ test = hypothesis.example(case=case)(test)
348
+ return test
349
+
350
+
351
+ def generate_coverage_cases(
352
+ *,
353
+ operation: APIOperation,
354
+ generation_modes: list[GenerationMode],
355
+ auth_storage: AuthStorage | None,
356
+ as_strategy_kwargs: dict[str, Any],
357
+ generate_duplicate_query_parameters: bool,
358
+ unexpected_methods: set[str],
359
+ generation_config: GenerationConfig,
360
+ ) -> Generator[Case]:
361
+ from schemathesis.core.parameters import LOCATION_TO_CONTAINER
362
+
363
+ auth_context = auths.AuthContext(
364
+ operation=operation,
365
+ app=operation.app,
366
+ )
367
+ overrides = {
368
+ container: as_strategy_kwargs[container]
369
+ for container in LOCATION_TO_CONTAINER.values()
370
+ if container in as_strategy_kwargs
371
+ }
372
+ with warnings.catch_warnings():
373
+ warnings.filterwarnings(
374
+ "ignore", message=".*but this is not valid syntax for a Python regular expression.*", category=UserWarning
375
+ )
376
+ for case in _iter_coverage_cases(
377
+ operation=operation,
378
+ generation_modes=generation_modes,
379
+ generate_duplicate_query_parameters=generate_duplicate_query_parameters,
380
+ unexpected_methods=unexpected_methods,
381
+ generation_config=generation_config,
382
+ ):
383
+ if case.media_type and operation.schema.transport.get_first_matching_media_type(case.media_type) is None:
384
+ continue
385
+ adjust_urlencoded_payload(case)
386
+ auths.set_on_case(case, auth_context, auth_storage)
387
+ for container_name, value in overrides.items():
388
+ container = getattr(case, container_name)
389
+ if container is None:
390
+ setattr(case, container_name, value)
391
+ else:
392
+ container.update(value)
393
+ yield case
394
+
395
+
396
+ class Instant:
397
+ __slots__ = ("start",)
398
+
399
+ def __init__(self) -> None:
400
+ self.start = perf_counter()
401
+
402
+ @property
403
+ def elapsed(self) -> float:
404
+ return perf_counter() - self.start
405
+
406
+
407
+ class Template:
408
+ __slots__ = ("_components", "_template", "_serializers")
409
+
410
+ def __init__(self, serializers: dict[str, Callable]) -> None:
411
+ self._components: dict[ParameterLocation, ComponentInfo] = {}
412
+ self._template: dict[str, Any] = {}
413
+ self._serializers = serializers
414
+
415
+ def __contains__(self, key: str) -> bool:
416
+ return key in self._template
417
+
418
+ def __getitem__(self, key: str) -> dict:
419
+ return self._template[key]
420
+
421
+ def get(self, key: str, default: Any = None) -> dict:
422
+ return self._template.get(key, default)
423
+
424
+ def add_parameter(self, location: ParameterLocation, name: str, value: coverage.GeneratedValue) -> None:
425
+ info = self._components.get(location)
426
+ if info is None:
427
+ self._components[location] = ComponentInfo(mode=value.generation_mode)
428
+ elif value.generation_mode == GenerationMode.NEGATIVE:
429
+ info.mode = GenerationMode.NEGATIVE
430
+
431
+ container = self._template.setdefault(location.container_name, {})
432
+ container[name] = value.value
433
+
434
+ def set_body(self, body: coverage.GeneratedValue, media_type: str) -> None:
435
+ self._template["body"] = body.value
436
+ self._template["media_type"] = media_type
437
+ self._components[ParameterLocation.BODY] = ComponentInfo(mode=body.generation_mode)
438
+
439
+ def _serialize(self, kwargs: dict[str, Any]) -> dict[str, Any]:
440
+ from schemathesis.specs.openapi._hypothesis import quote_all
441
+
442
+ output = {}
443
+ for container_name, value in kwargs.items():
444
+ serializer = self._serializers.get(container_name)
445
+ if container_name in ("headers", "cookies") and isinstance(value, dict):
446
+ value = _stringify_value(value, container_name)
447
+ if serializer is not None:
448
+ value = serializer(value)
449
+ if container_name == "query" and isinstance(value, dict):
450
+ value = _stringify_value(value, container_name)
451
+ if container_name == "path_parameters" and isinstance(value, dict):
452
+ value = _stringify_value(quote_all(value), container_name)
453
+ output[container_name] = value
454
+ return output
455
+
456
+ def unmodified(self) -> TemplateValue:
457
+ kwargs = deepclone(self._template)
458
+ kwargs = self._serialize(kwargs)
459
+ return TemplateValue(kwargs=kwargs, components=self._components.copy())
460
+
461
+ def with_body(self, *, media_type: str, value: coverage.GeneratedValue) -> TemplateValue:
462
+ kwargs = {**self._template, "media_type": media_type, "body": value.value}
463
+ kwargs = self._serialize(kwargs)
464
+ components = {**self._components, ParameterLocation.BODY: ComponentInfo(mode=value.generation_mode)}
465
+ return TemplateValue(kwargs=kwargs, components=components)
466
+
467
+ def with_parameter(
468
+ self, *, location: ParameterLocation, name: str, value: coverage.GeneratedValue
469
+ ) -> TemplateValue:
470
+ container = self._template[location.container_name]
471
+ return self.with_location(
472
+ location=location,
473
+ value={**container, name: value.value},
474
+ generation_mode=value.generation_mode,
475
+ )
476
+
477
+ def with_location(
478
+ self, *, location: ParameterLocation, value: Any, generation_mode: GenerationMode
479
+ ) -> TemplateValue:
480
+ kwargs = {**self._template, location.container_name: value}
481
+ components = {**self._components, location: ComponentInfo(mode=generation_mode)}
482
+ kwargs = self._serialize(kwargs)
483
+ return TemplateValue(kwargs=kwargs, components=components)
484
+
485
+
486
+ @dataclass
487
+ class TemplateValue:
488
+ kwargs: dict[str, Any]
489
+ components: dict[ParameterLocation, ComponentInfo]
490
+
491
+ __slots__ = ("kwargs", "components")
492
+
493
+
494
+ def _stringify_value(val: Any, container_name: str) -> Any:
495
+ if val is None:
496
+ return "null"
497
+ if val is True:
498
+ return "true"
499
+ if val is False:
500
+ return "false"
501
+ if isinstance(val, (int, float)):
502
+ return str(val)
503
+ if isinstance(val, list):
504
+ if container_name == "query":
505
+ # Having a list here ensures there will be multiple query parameters wit the same name
506
+ return [_stringify_value(item, container_name) for item in val]
507
+ # use comma-separated values style for arrays
508
+ return ",".join(str(_stringify_value(sub, container_name)) for sub in val)
509
+ if isinstance(val, dict):
510
+ return {key: _stringify_value(sub, container_name) for key, sub in val.items()}
511
+ return val
512
+
513
+
514
+ def _iter_coverage_cases(
515
+ *,
516
+ operation: APIOperation,
517
+ generation_modes: list[GenerationMode],
518
+ generate_duplicate_query_parameters: bool,
519
+ unexpected_methods: set[str],
520
+ generation_config: GenerationConfig,
521
+ ) -> Generator[Case, None, None]:
522
+ from schemathesis.specs.openapi._hypothesis import _build_custom_formats
523
+ from schemathesis.specs.openapi.examples import find_matching_in_responses
524
+ from schemathesis.specs.openapi.media_types import MEDIA_TYPES
525
+ from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
526
+ from schemathesis.specs.openapi.serialization import get_serializers_for_operation
527
+
528
+ generators: dict[tuple[ParameterLocation, str], Generator[coverage.GeneratedValue, None, None]] = {}
529
+ serializers = get_serializers_for_operation(operation)
530
+ template = Template(serializers)
531
+
532
+ instant = Instant()
533
+ responses = list(operation.responses.iter_examples())
534
+ custom_formats = _build_custom_formats(generation_config)
535
+
536
+ seen_negative = coverage.HashSet()
537
+ seen_positive = coverage.HashSet()
538
+ assert isinstance(operation.schema, BaseOpenAPISchema)
539
+ validator_cls = operation.schema.adapter.jsonschema_validator_cls
540
+
541
+ for parameter in operation.iter_parameters():
542
+ location = parameter.location
543
+ name = parameter.name
544
+ schema = parameter.unoptimized_schema
545
+ examples = parameter.examples
546
+ if examples:
547
+ schema = dict(schema)
548
+ schema["examples"] = examples
549
+ for value in find_matching_in_responses(responses, parameter.name):
550
+ schema.setdefault("examples", []).append(value)
551
+ gen = coverage.cover_schema_iter(
552
+ coverage.CoverageContext(
553
+ root_schema=schema,
554
+ location=location,
555
+ media_type=None,
556
+ generation_modes=generation_modes,
557
+ is_required=parameter.is_required,
558
+ custom_formats=custom_formats,
559
+ validator_cls=validator_cls,
560
+ allow_extra_parameters=generation_config.allow_extra_parameters,
561
+ ),
562
+ schema,
563
+ )
564
+ value = next(gen, NOT_SET)
565
+ if isinstance(value, NotSet):
566
+ if location == ParameterLocation.PATH:
567
+ # Can't skip path parameters - they should be filled
568
+ schema = dict(schema)
569
+ schema.setdefault("type", "string")
570
+ schema.setdefault("minLength", 1)
571
+ gen = coverage.cover_schema_iter(
572
+ coverage.CoverageContext(
573
+ root_schema=schema,
574
+ location=location,
575
+ media_type=None,
576
+ generation_modes=[GenerationMode.POSITIVE],
577
+ is_required=parameter.is_required,
578
+ custom_formats=custom_formats,
579
+ validator_cls=validator_cls,
580
+ allow_extra_parameters=generation_config.allow_extra_parameters,
581
+ ),
582
+ schema,
583
+ )
584
+ value = next(
585
+ gen,
586
+ coverage.GeneratedValue(
587
+ "value",
588
+ generation_mode=GenerationMode.NEGATIVE,
589
+ scenario=CoverageScenario.UNSUPPORTED_PATH_PATTERN,
590
+ description="Sample value for unsupported path parameter pattern",
591
+ parameter=name,
592
+ location="/",
593
+ ),
594
+ )
595
+ template.add_parameter(location, name, value)
596
+ continue
597
+ continue
598
+ template.add_parameter(location, name, value)
599
+ generators[(location, name)] = gen
600
+ template_time = instant.elapsed
601
+ if operation.body:
602
+ for body in operation.body:
603
+ instant = Instant()
604
+ schema = body.unoptimized_schema
605
+ examples = body.examples
606
+ if examples:
607
+ schema = dict(schema)
608
+ # User-registered media types should only handle text / binary data
609
+ if body.media_type in MEDIA_TYPES:
610
+ schema["examples"] = [example for example in examples if isinstance(example, (str, bytes))]
611
+ else:
612
+ schema["examples"] = examples
613
+ try:
614
+ media_type = media_types.parse(body.media_type)
615
+ except MalformedMediaType:
616
+ media_type = None
617
+ gen = coverage.cover_schema_iter(
618
+ coverage.CoverageContext(
619
+ root_schema=schema,
620
+ location=ParameterLocation.BODY,
621
+ media_type=media_type,
622
+ generation_modes=generation_modes,
623
+ is_required=body.is_required,
624
+ custom_formats=custom_formats,
625
+ validator_cls=validator_cls,
626
+ allow_extra_parameters=generation_config.allow_extra_parameters,
627
+ ),
628
+ schema,
629
+ )
630
+ value = next(gen, NOT_SET)
631
+ if isinstance(value, NotSet) or (
632
+ body.media_type in MEDIA_TYPES and not isinstance(value.value, (str, bytes))
633
+ ):
634
+ continue
635
+ elapsed = instant.elapsed
636
+ if "body" not in template:
637
+ template_time += elapsed
638
+ template.set_body(value, body.media_type)
639
+ data = template.with_body(value=value, media_type=body.media_type)
640
+ yield operation.Case(
641
+ **data.kwargs,
642
+ _meta=CaseMetadata(
643
+ generation=GenerationInfo(
644
+ time=elapsed,
645
+ mode=value.generation_mode,
646
+ ),
647
+ components=data.components,
648
+ phase=PhaseInfo.coverage(
649
+ scenario=value.scenario,
650
+ description=value.description,
651
+ location=value.location,
652
+ parameter=body.media_type,
653
+ parameter_location=ParameterLocation.BODY,
654
+ ),
655
+ ),
656
+ )
657
+ iterator = iter(gen)
658
+ while True:
659
+ instant = Instant()
660
+ try:
661
+ next_value = next(iterator)
662
+ if body.media_type in MEDIA_TYPES and not isinstance(next_value.value, (str, bytes)):
663
+ continue
664
+
665
+ data = template.with_body(value=next_value, media_type=body.media_type)
666
+ yield operation.Case(
667
+ **data.kwargs,
668
+ _meta=CaseMetadata(
669
+ generation=GenerationInfo(
670
+ time=instant.elapsed,
671
+ mode=next_value.generation_mode,
672
+ ),
673
+ components=data.components,
674
+ phase=PhaseInfo.coverage(
675
+ scenario=next_value.scenario,
676
+ description=next_value.description,
677
+ location=next_value.location,
678
+ parameter=body.media_type,
679
+ parameter_location=ParameterLocation.BODY,
680
+ ),
681
+ ),
682
+ )
683
+ except StopIteration:
684
+ break
685
+ elif GenerationMode.POSITIVE in generation_modes:
686
+ data = template.unmodified()
687
+ seen_positive.insert(data.kwargs)
688
+ yield operation.Case(
689
+ **data.kwargs,
690
+ _meta=CaseMetadata(
691
+ generation=GenerationInfo(
692
+ time=template_time,
693
+ mode=GenerationMode.POSITIVE,
694
+ ),
695
+ components=data.components,
696
+ phase=PhaseInfo.coverage(
697
+ scenario=CoverageScenario.DEFAULT_POSITIVE_TEST, description="Default positive test case"
698
+ ),
699
+ ),
700
+ )
701
+
702
+ for (location, name), gen in generators.items():
703
+ iterator = iter(gen)
704
+ while True:
705
+ instant = Instant()
706
+ try:
707
+ value = next(iterator)
708
+ data = template.with_parameter(location=location, name=name, value=value)
709
+ except StopIteration:
710
+ break
711
+
712
+ if value.generation_mode == GenerationMode.NEGATIVE:
713
+ seen_negative.insert(data.kwargs)
714
+ elif value.generation_mode == GenerationMode.POSITIVE and not seen_positive.insert(data.kwargs):
715
+ # Was already generated before
716
+ continue
717
+
718
+ yield operation.Case(
719
+ **data.kwargs,
720
+ _meta=CaseMetadata(
721
+ generation=GenerationInfo(time=instant.elapsed, mode=value.generation_mode),
722
+ components=data.components,
723
+ phase=PhaseInfo.coverage(
724
+ scenario=value.scenario,
725
+ description=value.description,
726
+ location=value.location,
727
+ parameter=name,
728
+ parameter_location=location,
729
+ ),
730
+ ),
731
+ )
732
+ if GenerationMode.NEGATIVE in generation_modes:
733
+ # Generate HTTP methods that are not specified in the spec
734
+ methods = unexpected_methods - set(operation.schema[operation.path])
735
+ for method in sorted(methods):
736
+ instant = Instant()
737
+ data = template.unmodified()
738
+ yield operation.Case(
739
+ **data.kwargs,
740
+ method=method.upper(),
741
+ _meta=CaseMetadata(
742
+ generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
743
+ components=data.components,
744
+ phase=PhaseInfo.coverage(
745
+ scenario=CoverageScenario.UNSPECIFIED_HTTP_METHOD,
746
+ description=f"Unspecified HTTP method: {method.upper()}",
747
+ ),
748
+ ),
749
+ )
750
+ # Generate duplicate query parameters
751
+ # NOTE: if the query schema has no constraints, then we may have no negative test cases at all
752
+ # as they all will match the original schema and therefore will be considered as positive ones
753
+ if generate_duplicate_query_parameters and operation.query and "query" in template:
754
+ container = template["query"]
755
+ for parameter in operation.query:
756
+ instant = Instant()
757
+ # Could be absent if value schema can't be negated
758
+ # I.e. contains just `default` value without any other keywords
759
+ value = container.get(parameter.name, NOT_SET)
760
+ if value is not NOT_SET:
761
+ data = template.with_location(
762
+ location=ParameterLocation.QUERY,
763
+ value={**container, parameter.name: [value, value]},
764
+ generation_mode=GenerationMode.NEGATIVE,
765
+ )
766
+ yield operation.Case(
767
+ **data.kwargs,
768
+ _meta=CaseMetadata(
769
+ generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
770
+ components=data.components,
771
+ phase=PhaseInfo.coverage(
772
+ scenario=CoverageScenario.DUPLICATE_PARAMETER,
773
+ description=f"Duplicate `{parameter.name}` query parameter",
774
+ parameter=parameter.name,
775
+ parameter_location=ParameterLocation.QUERY,
776
+ ),
777
+ ),
778
+ )
779
+ # Generate missing required parameters
780
+ for parameter in operation.iter_parameters():
781
+ if parameter.is_required and parameter.location != ParameterLocation.PATH:
782
+ instant = Instant()
783
+ name = parameter.name
784
+ location = parameter.location
785
+ container = template.get(location.container_name, {})
786
+ data = template.with_location(
787
+ location=location,
788
+ value={k: v for k, v in container.items() if k != name},
789
+ generation_mode=GenerationMode.NEGATIVE,
790
+ )
791
+
792
+ if seen_negative.insert(data.kwargs):
793
+ yield operation.Case(
794
+ **data.kwargs,
795
+ _meta=CaseMetadata(
796
+ generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
797
+ components=data.components,
798
+ phase=PhaseInfo.coverage(
799
+ scenario=CoverageScenario.MISSING_PARAMETER,
800
+ description=f"Missing `{name}` at {location.value}",
801
+ parameter=name,
802
+ parameter_location=location,
803
+ ),
804
+ ),
805
+ )
806
+ # Generate combinations for each location
807
+ for location, parameter_set in [
808
+ (ParameterLocation.QUERY, operation.query),
809
+ (ParameterLocation.HEADER, operation.headers),
810
+ (ParameterLocation.COOKIE, operation.cookies),
811
+ ]:
812
+ if not parameter_set:
813
+ continue
814
+
815
+ container_name = location.container_name
816
+ base_container = template.get(container_name, {})
817
+
818
+ # Get required and optional parameters
819
+ required = {p.name for p in parameter_set if p.is_required}
820
+ all_params = {p.name for p in parameter_set}
821
+ optional = sorted(all_params - required)
822
+
823
+ # Helper function to create and yield a case
824
+ def make_case(
825
+ container_values: dict,
826
+ scenario: CoverageScenario,
827
+ description: str,
828
+ _location: ParameterLocation,
829
+ _parameter: str | None,
830
+ _generation_mode: GenerationMode,
831
+ _instant: Instant,
832
+ ) -> Case:
833
+ data = template.with_location(location=_location, value=container_values, generation_mode=_generation_mode)
834
+ return operation.Case(
835
+ **data.kwargs,
836
+ _meta=CaseMetadata(
837
+ generation=GenerationInfo(
838
+ time=_instant.elapsed,
839
+ mode=_generation_mode,
840
+ ),
841
+ components=data.components,
842
+ phase=PhaseInfo.coverage(
843
+ scenario=scenario,
844
+ description=description,
845
+ parameter=_parameter,
846
+ parameter_location=_location,
847
+ ),
848
+ ),
849
+ )
850
+
851
+ def _combination_schema(
852
+ combination: dict[str, Any], _required: set[str], _parameter_set: ParameterSet
853
+ ) -> dict[str, Any]:
854
+ return {
855
+ "properties": {
856
+ parameter.name: parameter.optimized_schema
857
+ for parameter in _parameter_set
858
+ if parameter.name in combination
859
+ },
860
+ "required": list(_required),
861
+ "additionalProperties": False,
862
+ }
863
+
864
+ def _yield_negative(
865
+ subschema: dict[str, Any], _location: ParameterLocation, is_required: bool
866
+ ) -> Generator[Case, None, None]:
867
+ iterator = iter(
868
+ coverage.cover_schema_iter(
869
+ coverage.CoverageContext(
870
+ root_schema=subschema,
871
+ location=_location,
872
+ media_type=None,
873
+ generation_modes=[GenerationMode.NEGATIVE],
874
+ is_required=is_required,
875
+ custom_formats=custom_formats,
876
+ validator_cls=validator_cls,
877
+ allow_extra_parameters=generation_config.allow_extra_parameters,
878
+ ),
879
+ subschema,
880
+ )
881
+ )
882
+ while True:
883
+ instant = Instant()
884
+ try:
885
+ more = next(iterator)
886
+ yield make_case(
887
+ more.value,
888
+ more.scenario,
889
+ more.description,
890
+ _location,
891
+ more.parameter,
892
+ GenerationMode.NEGATIVE,
893
+ instant,
894
+ )
895
+ except StopIteration:
896
+ break
897
+
898
+ # 1. Generate only required properties
899
+ if required and all_params != required:
900
+ only_required = {k: v for k, v in base_container.items() if k in required}
901
+ if GenerationMode.POSITIVE in generation_modes:
902
+ yield make_case(
903
+ only_required,
904
+ CoverageScenario.OBJECT_ONLY_REQUIRED,
905
+ "Only required properties",
906
+ location,
907
+ None,
908
+ GenerationMode.POSITIVE,
909
+ Instant(),
910
+ )
911
+ if GenerationMode.NEGATIVE in generation_modes:
912
+ subschema = _combination_schema(only_required, required, parameter_set)
913
+ for case in _yield_negative(subschema, location, is_required=bool(required)):
914
+ kwargs = _case_to_kwargs(case)
915
+ if not seen_negative.insert(kwargs):
916
+ continue
917
+ assert case.meta is not None
918
+ assert isinstance(case.meta.phase.data, CoveragePhaseData)
919
+ # Already generated in one of the blocks above
920
+ if (
921
+ location != "path"
922
+ and case.meta.phase.data.scenario != CoverageScenario.OBJECT_MISSING_REQUIRED_PROPERTY
923
+ ):
924
+ yield case
925
+
926
+ # 2. Generate combinations with required properties and one optional property
927
+ for opt_param in optional:
928
+ combo = {k: v for k, v in base_container.items() if k in required or k == opt_param}
929
+ if combo != base_container and GenerationMode.POSITIVE in generation_modes:
930
+ yield make_case(
931
+ combo,
932
+ CoverageScenario.OBJECT_REQUIRED_AND_OPTIONAL,
933
+ f"All required properties and optional '{opt_param}'",
934
+ location,
935
+ None,
936
+ GenerationMode.POSITIVE,
937
+ Instant(),
938
+ )
939
+ if GenerationMode.NEGATIVE in generation_modes:
940
+ subschema = _combination_schema(combo, required, parameter_set)
941
+ for case in _yield_negative(subschema, location, is_required=bool(required)):
942
+ assert case.meta is not None
943
+ assert isinstance(case.meta.phase.data, CoveragePhaseData)
944
+ # Already generated in one of the blocks above
945
+ if (
946
+ location != "path"
947
+ and case.meta.phase.data.scenario != CoverageScenario.OBJECT_MISSING_REQUIRED_PROPERTY
948
+ ):
949
+ yield case
950
+
951
+ # 3. Generate one combination for each size from 2 to N-1 of optional parameters
952
+ if len(optional) > 1 and GenerationMode.POSITIVE in generation_modes:
953
+ for size in range(2, len(optional)):
954
+ for combination in combinations(optional, size):
955
+ combo = {k: v for k, v in base_container.items() if k in required or k in combination}
956
+ if combo != base_container:
957
+ yield make_case(
958
+ combo,
959
+ CoverageScenario.OBJECT_REQUIRED_AND_OPTIONAL,
960
+ f"All required and {size} optional properties",
961
+ location,
962
+ None,
963
+ GenerationMode.POSITIVE,
964
+ Instant(),
965
+ )
966
+
967
+
968
+ def _case_to_kwargs(case: Case) -> dict:
969
+ kwargs = {}
970
+ for container_name in LOCATION_TO_CONTAINER.values():
971
+ value = getattr(case, container_name)
972
+ if isinstance(value, CaseInsensitiveDict) and value:
973
+ kwargs[container_name] = dict(value)
974
+ elif value and value is not NOT_SET:
975
+ kwargs[container_name] = value
976
+ return kwargs
977
+
978
+
979
+ def find_invalid_headers(headers: Mapping) -> Generator[tuple[str, str], None, None]:
980
+ for name, value in headers.items():
981
+ if not is_latin_1_encodable(value) or has_invalid_characters(name, value):
982
+ yield name, value
983
+
984
+
985
+ UnsatisfiableExampleMark = Mark[Unsatisfiable](attr_name="unsatisfiable_example")
986
+ NonSerializableMark = Mark[SerializationNotPossible](attr_name="non_serializable")
987
+ InvalidRegexMark = Mark[SchemaError](attr_name="invalid_regex")
988
+ InvalidHeadersExampleMark = Mark[dict[str, str]](attr_name="invalid_example_header")
989
+ MissingPathParameters = Mark[InvalidSchema](attr_name="missing_path_parameters")
990
+ InfiniteRecursiveReferenceMark = Mark[InfiniteRecursiveReference](attr_name="infinite_recursive_reference")
991
+ UnresolvableReferenceMark = Mark[UnresolvableReference](attr_name="unresolvable_reference")
992
+ ApiOperationMark = Mark[APIOperation](attr_name="api_operation")