schemathesis 3.39.16__py3-none-any.whl → 4.0.0__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 (255) hide show
  1. schemathesis/__init__.py +41 -79
  2. schemathesis/auths.py +111 -122
  3. schemathesis/checks.py +169 -60
  4. schemathesis/cli/__init__.py +15 -2117
  5. schemathesis/cli/commands/__init__.py +85 -0
  6. schemathesis/cli/commands/data.py +10 -0
  7. schemathesis/cli/commands/run/__init__.py +590 -0
  8. schemathesis/cli/commands/run/context.py +204 -0
  9. schemathesis/cli/commands/run/events.py +60 -0
  10. schemathesis/cli/commands/run/executor.py +157 -0
  11. schemathesis/cli/commands/run/filters.py +53 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
  15. schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1628 -0
  17. schemathesis/cli/commands/run/loaders.py +114 -0
  18. schemathesis/cli/commands/run/validation.py +246 -0
  19. schemathesis/cli/constants.py +5 -58
  20. schemathesis/cli/core.py +19 -0
  21. schemathesis/cli/ext/fs.py +16 -0
  22. schemathesis/cli/ext/groups.py +84 -0
  23. schemathesis/cli/{options.py → ext/options.py} +36 -34
  24. schemathesis/config/__init__.py +189 -0
  25. schemathesis/config/_auth.py +51 -0
  26. schemathesis/config/_checks.py +268 -0
  27. schemathesis/config/_diff_base.py +99 -0
  28. schemathesis/config/_env.py +21 -0
  29. schemathesis/config/_error.py +156 -0
  30. schemathesis/config/_generation.py +149 -0
  31. schemathesis/config/_health_check.py +24 -0
  32. schemathesis/config/_operations.py +327 -0
  33. schemathesis/config/_output.py +171 -0
  34. schemathesis/config/_parameters.py +19 -0
  35. schemathesis/config/_phases.py +187 -0
  36. schemathesis/config/_projects.py +527 -0
  37. schemathesis/config/_rate_limit.py +17 -0
  38. schemathesis/config/_report.py +120 -0
  39. schemathesis/config/_validator.py +9 -0
  40. schemathesis/config/_warnings.py +25 -0
  41. schemathesis/config/schema.json +885 -0
  42. schemathesis/core/__init__.py +67 -0
  43. schemathesis/core/compat.py +32 -0
  44. schemathesis/core/control.py +2 -0
  45. schemathesis/core/curl.py +58 -0
  46. schemathesis/core/deserialization.py +65 -0
  47. schemathesis/core/errors.py +459 -0
  48. schemathesis/core/failures.py +315 -0
  49. schemathesis/core/fs.py +19 -0
  50. schemathesis/core/hooks.py +20 -0
  51. schemathesis/core/loaders.py +104 -0
  52. schemathesis/core/marks.py +66 -0
  53. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  54. schemathesis/core/output/__init__.py +46 -0
  55. schemathesis/core/output/sanitization.py +54 -0
  56. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  57. schemathesis/core/registries.py +31 -0
  58. schemathesis/core/transforms.py +113 -0
  59. schemathesis/core/transport.py +223 -0
  60. schemathesis/core/validation.py +54 -0
  61. schemathesis/core/version.py +7 -0
  62. schemathesis/engine/__init__.py +28 -0
  63. schemathesis/engine/context.py +118 -0
  64. schemathesis/engine/control.py +36 -0
  65. schemathesis/engine/core.py +169 -0
  66. schemathesis/engine/errors.py +464 -0
  67. schemathesis/engine/events.py +258 -0
  68. schemathesis/engine/phases/__init__.py +88 -0
  69. schemathesis/{runner → engine/phases}/probes.py +52 -68
  70. schemathesis/engine/phases/stateful/__init__.py +68 -0
  71. schemathesis/engine/phases/stateful/_executor.py +356 -0
  72. schemathesis/engine/phases/stateful/context.py +85 -0
  73. schemathesis/engine/phases/unit/__init__.py +212 -0
  74. schemathesis/engine/phases/unit/_executor.py +416 -0
  75. schemathesis/engine/phases/unit/_pool.py +82 -0
  76. schemathesis/engine/recorder.py +247 -0
  77. schemathesis/errors.py +43 -0
  78. schemathesis/filters.py +17 -98
  79. schemathesis/generation/__init__.py +5 -33
  80. schemathesis/generation/case.py +317 -0
  81. schemathesis/generation/coverage.py +282 -175
  82. schemathesis/generation/hypothesis/__init__.py +36 -0
  83. schemathesis/generation/hypothesis/builder.py +800 -0
  84. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  85. schemathesis/generation/hypothesis/given.py +66 -0
  86. schemathesis/generation/hypothesis/reporting.py +14 -0
  87. schemathesis/generation/hypothesis/strategies.py +16 -0
  88. schemathesis/generation/meta.py +115 -0
  89. schemathesis/generation/metrics.py +93 -0
  90. schemathesis/generation/modes.py +20 -0
  91. schemathesis/generation/overrides.py +116 -0
  92. schemathesis/generation/stateful/__init__.py +37 -0
  93. schemathesis/generation/stateful/state_machine.py +278 -0
  94. schemathesis/graphql/__init__.py +15 -0
  95. schemathesis/graphql/checks.py +109 -0
  96. schemathesis/graphql/loaders.py +284 -0
  97. schemathesis/hooks.py +80 -101
  98. schemathesis/openapi/__init__.py +13 -0
  99. schemathesis/openapi/checks.py +455 -0
  100. schemathesis/openapi/generation/__init__.py +0 -0
  101. schemathesis/openapi/generation/filters.py +72 -0
  102. schemathesis/openapi/loaders.py +313 -0
  103. schemathesis/pytest/__init__.py +5 -0
  104. schemathesis/pytest/control_flow.py +7 -0
  105. schemathesis/pytest/lazy.py +281 -0
  106. schemathesis/pytest/loaders.py +36 -0
  107. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
  108. schemathesis/python/__init__.py +0 -0
  109. schemathesis/python/asgi.py +12 -0
  110. schemathesis/python/wsgi.py +12 -0
  111. schemathesis/schemas.py +537 -273
  112. schemathesis/specs/graphql/__init__.py +0 -1
  113. schemathesis/specs/graphql/_cache.py +1 -2
  114. schemathesis/specs/graphql/scalars.py +42 -6
  115. schemathesis/specs/graphql/schemas.py +141 -137
  116. schemathesis/specs/graphql/validation.py +11 -17
  117. schemathesis/specs/openapi/__init__.py +6 -1
  118. schemathesis/specs/openapi/_cache.py +1 -2
  119. schemathesis/specs/openapi/_hypothesis.py +142 -156
  120. schemathesis/specs/openapi/checks.py +368 -257
  121. schemathesis/specs/openapi/converter.py +4 -4
  122. schemathesis/specs/openapi/definitions.py +1 -1
  123. schemathesis/specs/openapi/examples.py +23 -21
  124. schemathesis/specs/openapi/expressions/__init__.py +31 -19
  125. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  126. schemathesis/specs/openapi/expressions/lexer.py +1 -1
  127. schemathesis/specs/openapi/expressions/nodes.py +36 -41
  128. schemathesis/specs/openapi/expressions/parser.py +1 -1
  129. schemathesis/specs/openapi/formats.py +35 -7
  130. schemathesis/specs/openapi/media_types.py +53 -12
  131. schemathesis/specs/openapi/negative/__init__.py +7 -4
  132. schemathesis/specs/openapi/negative/mutations.py +6 -5
  133. schemathesis/specs/openapi/parameters.py +7 -10
  134. schemathesis/specs/openapi/patterns.py +94 -31
  135. schemathesis/specs/openapi/references.py +12 -53
  136. schemathesis/specs/openapi/schemas.py +233 -307
  137. schemathesis/specs/openapi/security.py +1 -1
  138. schemathesis/specs/openapi/serialization.py +12 -6
  139. schemathesis/specs/openapi/stateful/__init__.py +268 -133
  140. schemathesis/specs/openapi/stateful/control.py +87 -0
  141. schemathesis/specs/openapi/stateful/links.py +209 -0
  142. schemathesis/transport/__init__.py +142 -0
  143. schemathesis/transport/asgi.py +26 -0
  144. schemathesis/transport/prepare.py +124 -0
  145. schemathesis/transport/requests.py +244 -0
  146. schemathesis/{_xml.py → transport/serialization.py} +69 -11
  147. schemathesis/transport/wsgi.py +171 -0
  148. schemathesis-4.0.0.dist-info/METADATA +204 -0
  149. schemathesis-4.0.0.dist-info/RECORD +164 -0
  150. {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
  151. {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
  152. schemathesis/_compat.py +0 -74
  153. schemathesis/_dependency_versions.py +0 -19
  154. schemathesis/_hypothesis.py +0 -717
  155. schemathesis/_override.py +0 -50
  156. schemathesis/_patches.py +0 -21
  157. schemathesis/_rate_limiter.py +0 -7
  158. schemathesis/cli/callbacks.py +0 -466
  159. schemathesis/cli/cassettes.py +0 -561
  160. schemathesis/cli/context.py +0 -75
  161. schemathesis/cli/debug.py +0 -27
  162. schemathesis/cli/handlers.py +0 -19
  163. schemathesis/cli/junitxml.py +0 -124
  164. schemathesis/cli/output/__init__.py +0 -1
  165. schemathesis/cli/output/default.py +0 -920
  166. schemathesis/cli/output/short.py +0 -59
  167. schemathesis/cli/reporting.py +0 -79
  168. schemathesis/cli/sanitization.py +0 -26
  169. schemathesis/code_samples.py +0 -151
  170. schemathesis/constants.py +0 -54
  171. schemathesis/contrib/__init__.py +0 -11
  172. schemathesis/contrib/openapi/__init__.py +0 -11
  173. schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
  174. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  175. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  176. schemathesis/contrib/unique_data.py +0 -41
  177. schemathesis/exceptions.py +0 -571
  178. schemathesis/experimental/__init__.py +0 -109
  179. schemathesis/extra/_aiohttp.py +0 -28
  180. schemathesis/extra/_flask.py +0 -13
  181. schemathesis/extra/_server.py +0 -18
  182. schemathesis/failures.py +0 -284
  183. schemathesis/fixups/__init__.py +0 -37
  184. schemathesis/fixups/fast_api.py +0 -41
  185. schemathesis/fixups/utf8_bom.py +0 -28
  186. schemathesis/generation/_methods.py +0 -44
  187. schemathesis/graphql.py +0 -3
  188. schemathesis/internal/__init__.py +0 -7
  189. schemathesis/internal/checks.py +0 -86
  190. schemathesis/internal/copy.py +0 -32
  191. schemathesis/internal/datetime.py +0 -5
  192. schemathesis/internal/deprecation.py +0 -37
  193. schemathesis/internal/diff.py +0 -15
  194. schemathesis/internal/extensions.py +0 -27
  195. schemathesis/internal/jsonschema.py +0 -36
  196. schemathesis/internal/output.py +0 -68
  197. schemathesis/internal/transformation.py +0 -26
  198. schemathesis/internal/validation.py +0 -34
  199. schemathesis/lazy.py +0 -474
  200. schemathesis/loaders.py +0 -122
  201. schemathesis/models.py +0 -1341
  202. schemathesis/parameters.py +0 -90
  203. schemathesis/runner/__init__.py +0 -605
  204. schemathesis/runner/events.py +0 -389
  205. schemathesis/runner/impl/__init__.py +0 -3
  206. schemathesis/runner/impl/context.py +0 -88
  207. schemathesis/runner/impl/core.py +0 -1280
  208. schemathesis/runner/impl/solo.py +0 -80
  209. schemathesis/runner/impl/threadpool.py +0 -391
  210. schemathesis/runner/serialization.py +0 -544
  211. schemathesis/sanitization.py +0 -252
  212. schemathesis/serializers.py +0 -328
  213. schemathesis/service/__init__.py +0 -18
  214. schemathesis/service/auth.py +0 -11
  215. schemathesis/service/ci.py +0 -202
  216. schemathesis/service/client.py +0 -133
  217. schemathesis/service/constants.py +0 -38
  218. schemathesis/service/events.py +0 -61
  219. schemathesis/service/extensions.py +0 -224
  220. schemathesis/service/hosts.py +0 -111
  221. schemathesis/service/metadata.py +0 -71
  222. schemathesis/service/models.py +0 -258
  223. schemathesis/service/report.py +0 -255
  224. schemathesis/service/serialization.py +0 -173
  225. schemathesis/service/usage.py +0 -66
  226. schemathesis/specs/graphql/loaders.py +0 -364
  227. schemathesis/specs/openapi/expressions/context.py +0 -16
  228. schemathesis/specs/openapi/links.py +0 -389
  229. schemathesis/specs/openapi/loaders.py +0 -707
  230. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  231. schemathesis/specs/openapi/stateful/types.py +0 -14
  232. schemathesis/specs/openapi/validation.py +0 -26
  233. schemathesis/stateful/__init__.py +0 -147
  234. schemathesis/stateful/config.py +0 -97
  235. schemathesis/stateful/context.py +0 -135
  236. schemathesis/stateful/events.py +0 -274
  237. schemathesis/stateful/runner.py +0 -309
  238. schemathesis/stateful/sink.py +0 -68
  239. schemathesis/stateful/state_machine.py +0 -328
  240. schemathesis/stateful/statistic.py +0 -22
  241. schemathesis/stateful/validation.py +0 -100
  242. schemathesis/targets.py +0 -77
  243. schemathesis/transports/__init__.py +0 -369
  244. schemathesis/transports/asgi.py +0 -7
  245. schemathesis/transports/auth.py +0 -38
  246. schemathesis/transports/headers.py +0 -36
  247. schemathesis/transports/responses.py +0 -57
  248. schemathesis/types.py +0 -44
  249. schemathesis/utils.py +0 -164
  250. schemathesis-3.39.16.dist-info/METADATA +0 -293
  251. schemathesis-3.39.16.dist-info/RECORD +0 -160
  252. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  253. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  254. /schemathesis/{internal → core}/result.py +0 -0
  255. {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,800 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from dataclasses import dataclass, field
5
+ from enum import Enum
6
+ from functools import wraps
7
+ from itertools import combinations
8
+ from time import perf_counter
9
+ from typing import Any, Callable, Generator, Mapping
10
+
11
+ import hypothesis
12
+ from hypothesis import Phase, Verbosity
13
+ from hypothesis import strategies as st
14
+ from hypothesis._settings import all_settings
15
+ from hypothesis.errors import Unsatisfiable
16
+ from jsonschema.exceptions import SchemaError
17
+ from requests.models import CaseInsensitiveDict
18
+
19
+ from schemathesis import auths
20
+ from schemathesis.auths import AuthStorage, AuthStorageMark
21
+ from schemathesis.config import ProjectConfig
22
+ from schemathesis.core import NOT_SET, NotSet, SpecificationFeature, media_types
23
+ from schemathesis.core.errors import InvalidSchema, SerializationNotPossible
24
+ from schemathesis.core.marks import Mark
25
+ from schemathesis.core.transport import prepare_urlencoded
26
+ from schemathesis.core.validation import has_invalid_characters, is_latin_1_encodable
27
+ from schemathesis.generation import GenerationMode, coverage
28
+ from schemathesis.generation.case import Case
29
+ from schemathesis.generation.hypothesis import DEFAULT_DEADLINE, examples, setup, strategies
30
+ from schemathesis.generation.hypothesis.examples import add_single_example
31
+ from schemathesis.generation.hypothesis.given import GivenInput
32
+ from schemathesis.generation.meta import (
33
+ CaseMetadata,
34
+ ComponentInfo,
35
+ ComponentKind,
36
+ CoveragePhaseData,
37
+ GenerationInfo,
38
+ PhaseInfo,
39
+ )
40
+ from schemathesis.hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookDispatcherMark
41
+ from schemathesis.schemas import APIOperation, ParameterSet
42
+
43
+ setup()
44
+
45
+
46
+ class HypothesisTestMode(str, Enum):
47
+ EXAMPLES = "examples"
48
+ COVERAGE = "coverage"
49
+ FUZZING = "fuzzing"
50
+
51
+
52
+ @dataclass
53
+ class HypothesisTestConfig:
54
+ project: ProjectConfig
55
+ modes: list[HypothesisTestMode]
56
+ settings: hypothesis.settings | None = None
57
+ seed: int | None = None
58
+ as_strategy_kwargs: dict[str, Any] = field(default_factory=dict)
59
+ given_args: tuple[GivenInput, ...] = ()
60
+ given_kwargs: dict[str, GivenInput] = field(default_factory=dict)
61
+
62
+
63
+ def create_test(
64
+ *,
65
+ operation: APIOperation,
66
+ test_func: Callable,
67
+ config: HypothesisTestConfig,
68
+ ) -> Callable:
69
+ """Create a Hypothesis test."""
70
+ hook_dispatcher = HookDispatcherMark.get(test_func)
71
+ auth_storage = AuthStorageMark.get(test_func)
72
+
73
+ strategy_kwargs = {
74
+ "hooks": hook_dispatcher,
75
+ "auth_storage": auth_storage,
76
+ **config.as_strategy_kwargs,
77
+ }
78
+ generation = config.project.generation_for(operation=operation)
79
+ strategy = strategies.combine(
80
+ [operation.as_strategy(generation_mode=mode, **strategy_kwargs) for mode in generation.modes]
81
+ )
82
+
83
+ hypothesis_test = create_base_test(
84
+ test_function=test_func,
85
+ strategy=strategy,
86
+ args=config.given_args,
87
+ kwargs=config.given_kwargs,
88
+ )
89
+
90
+ if config.seed is not None:
91
+ hypothesis_test = hypothesis.seed(config.seed)(hypothesis_test)
92
+
93
+ default = hypothesis.settings.default
94
+ settings = getattr(hypothesis_test, SETTINGS_ATTRIBUTE_NAME, None)
95
+ assert settings is not None
96
+
97
+ if settings.deadline == default.deadline:
98
+ settings = hypothesis.settings(settings, deadline=DEFAULT_DEADLINE)
99
+
100
+ if settings.verbosity == default.verbosity:
101
+ settings = hypothesis.settings(settings, verbosity=Verbosity.quiet)
102
+
103
+ if config.settings is not None:
104
+ # Merge the user-provided settings with the current ones
105
+ settings = hypothesis.settings(
106
+ config.settings,
107
+ **{
108
+ item: getattr(settings, item)
109
+ for item in all_settings
110
+ if getattr(settings, item) != getattr(default, item)
111
+ },
112
+ )
113
+
114
+ if Phase.explain in settings.phases:
115
+ phases = tuple(phase for phase in settings.phases if phase != Phase.explain)
116
+ settings = hypothesis.settings(settings, phases=phases)
117
+
118
+ # Remove `reuse` & `generate` phases to avoid yielding any test cases if we don't do fuzzing
119
+ if HypothesisTestMode.FUZZING not in config.modes and (
120
+ Phase.generate in settings.phases or Phase.reuse in settings.phases
121
+ ):
122
+ phases = tuple(phase for phase in settings.phases if phase not in (Phase.reuse, Phase.generate))
123
+ settings = hypothesis.settings(settings, phases=phases)
124
+
125
+ specification = operation.schema.specification
126
+
127
+ # Add examples if explicit phase is enabled
128
+ if (
129
+ HypothesisTestMode.EXAMPLES in config.modes
130
+ and Phase.explicit in settings.phases
131
+ and specification.supports_feature(SpecificationFeature.EXAMPLES)
132
+ ):
133
+ phases_config = config.project.phases_for(operation=operation)
134
+ hypothesis_test = add_examples(
135
+ hypothesis_test,
136
+ operation,
137
+ fill_missing=phases_config.examples.fill_missing,
138
+ hook_dispatcher=hook_dispatcher,
139
+ **strategy_kwargs,
140
+ )
141
+
142
+ if (
143
+ HypothesisTestMode.COVERAGE in config.modes
144
+ and Phase.explicit in settings.phases
145
+ and specification.supports_feature(SpecificationFeature.COVERAGE)
146
+ and not config.given_args
147
+ and not config.given_kwargs
148
+ ):
149
+ phases_config = config.project.phases_for(operation=operation)
150
+ hypothesis_test = add_coverage(
151
+ hypothesis_test,
152
+ operation,
153
+ generation.modes,
154
+ auth_storage,
155
+ config.as_strategy_kwargs,
156
+ generate_duplicate_query_parameters=phases_config.coverage.generate_duplicate_query_parameters,
157
+ unexpected_methods=phases_config.coverage.unexpected_methods,
158
+ )
159
+
160
+ setattr(hypothesis_test, SETTINGS_ATTRIBUTE_NAME, settings)
161
+
162
+ return hypothesis_test
163
+
164
+
165
+ SETTINGS_ATTRIBUTE_NAME = "_hypothesis_internal_use_settings"
166
+
167
+
168
+ def create_base_test(
169
+ *,
170
+ test_function: Callable,
171
+ strategy: st.SearchStrategy,
172
+ args: tuple[GivenInput, ...],
173
+ kwargs: dict[str, GivenInput],
174
+ ) -> Callable:
175
+ """Create the basic Hypothesis test with the given strategy."""
176
+
177
+ @wraps(test_function)
178
+ def test_wrapper(*args: Any, **kwargs: Any) -> Any:
179
+ __tracebackhide__ = True
180
+ return test_function(*args, **kwargs)
181
+
182
+ funcobj = hypothesis.given(*args, **{**kwargs, "case": strategy})(test_wrapper)
183
+
184
+ if asyncio.iscoroutinefunction(test_function):
185
+ funcobj.hypothesis.inner_test = make_async_test(test_function) # type: ignore
186
+ return funcobj
187
+
188
+
189
+ def make_async_test(test: Callable) -> Callable:
190
+ def async_run(*args: Any, **kwargs: Any) -> None:
191
+ try:
192
+ loop = asyncio.get_event_loop()
193
+ except RuntimeError:
194
+ loop = asyncio.new_event_loop()
195
+ coro = test(*args, **kwargs)
196
+ future = asyncio.ensure_future(coro, loop=loop)
197
+ loop.run_until_complete(future)
198
+
199
+ return async_run
200
+
201
+
202
+ def add_examples(
203
+ test: Callable,
204
+ operation: APIOperation,
205
+ fill_missing: bool,
206
+ hook_dispatcher: HookDispatcher | None = None,
207
+ **kwargs: Any,
208
+ ) -> Callable:
209
+ """Add examples to the Hypothesis test, if they are specified in the schema."""
210
+ from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
211
+
212
+ try:
213
+ result: list[Case] = [
214
+ examples.generate_one(strategy) for strategy in operation.get_strategies_from_examples(**kwargs)
215
+ ]
216
+ except (
217
+ InvalidSchema,
218
+ HypothesisRefResolutionError,
219
+ Unsatisfiable,
220
+ SerializationNotPossible,
221
+ SchemaError,
222
+ ) as exc:
223
+ result = []
224
+ if isinstance(exc, Unsatisfiable):
225
+ UnsatisfiableExampleMark.set(test, exc)
226
+ if isinstance(exc, SerializationNotPossible):
227
+ NonSerializableMark.set(test, exc)
228
+ if isinstance(exc, SchemaError):
229
+ InvalidRegexMark.set(test, exc)
230
+
231
+ if fill_missing and not result:
232
+ strategy = operation.as_strategy()
233
+ add_single_example(strategy, result)
234
+
235
+ context = HookContext(operation=operation) # context should be passed here instead
236
+ GLOBAL_HOOK_DISPATCHER.dispatch("before_add_examples", context, result)
237
+ operation.schema.hooks.dispatch("before_add_examples", context, result)
238
+ if hook_dispatcher:
239
+ hook_dispatcher.dispatch("before_add_examples", context, result)
240
+ original_test = test
241
+ for example in result:
242
+ if example.headers is not None:
243
+ invalid_headers = dict(find_invalid_headers(example.headers))
244
+ if invalid_headers:
245
+ InvalidHeadersExampleMark.set(original_test, invalid_headers)
246
+ continue
247
+ adjust_urlencoded_payload(example)
248
+ test = hypothesis.example(case=example)(test)
249
+
250
+ return test
251
+
252
+
253
+ def adjust_urlencoded_payload(case: Case) -> None:
254
+ if case.media_type is not None:
255
+ try:
256
+ media_type = media_types.parse(case.media_type)
257
+ if media_type == ("application", "x-www-form-urlencoded"):
258
+ case.body = prepare_urlencoded(case.body)
259
+ except ValueError:
260
+ pass
261
+
262
+
263
+ def add_coverage(
264
+ test: Callable,
265
+ operation: APIOperation,
266
+ generation_modes: list[GenerationMode],
267
+ auth_storage: AuthStorage | None,
268
+ as_strategy_kwargs: dict[str, Any],
269
+ generate_duplicate_query_parameters: bool,
270
+ unexpected_methods: set[str] | None = None,
271
+ ) -> Callable:
272
+ from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
273
+
274
+ auth_context = auths.AuthContext(
275
+ operation=operation,
276
+ app=operation.app,
277
+ )
278
+ overrides = {
279
+ container: as_strategy_kwargs[container]
280
+ for container in LOCATION_TO_CONTAINER.values()
281
+ if container in as_strategy_kwargs
282
+ }
283
+ for case in _iter_coverage_cases(
284
+ operation, generation_modes, generate_duplicate_query_parameters, unexpected_methods
285
+ ):
286
+ if case.media_type and operation.schema.transport.get_first_matching_media_type(case.media_type) is None:
287
+ continue
288
+ adjust_urlencoded_payload(case)
289
+ auths.set_on_case(case, auth_context, auth_storage)
290
+ for container_name, value in overrides.items():
291
+ container = getattr(case, container_name)
292
+ if container is None:
293
+ setattr(case, container_name, value)
294
+ else:
295
+ container.update(value)
296
+
297
+ test = hypothesis.example(case=case)(test)
298
+ return test
299
+
300
+
301
+ class Instant:
302
+ __slots__ = ("start",)
303
+
304
+ def __init__(self) -> None:
305
+ self.start = perf_counter()
306
+
307
+ @property
308
+ def elapsed(self) -> float:
309
+ return perf_counter() - self.start
310
+
311
+
312
+ class Template:
313
+ __slots__ = ("_components", "_template", "_serializers")
314
+
315
+ def __init__(self, serializers: dict[str, Callable]) -> None:
316
+ self._components: dict[ComponentKind, ComponentInfo] = {}
317
+ self._template: dict[str, Any] = {}
318
+ self._serializers = serializers
319
+
320
+ def __contains__(self, key: str) -> bool:
321
+ return key in self._template
322
+
323
+ def __getitem__(self, key: str) -> dict:
324
+ return self._template[key]
325
+
326
+ def get(self, key: str, default: Any = None) -> dict:
327
+ return self._template.get(key, default)
328
+
329
+ def add_parameter(self, location: str, name: str, value: coverage.GeneratedValue) -> None:
330
+ from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
331
+
332
+ component_name = LOCATION_TO_CONTAINER[location]
333
+ kind = ComponentKind(component_name)
334
+ info = self._components.get(kind)
335
+ if info is None:
336
+ self._components[kind] = ComponentInfo(mode=value.generation_mode)
337
+ elif value.generation_mode == GenerationMode.NEGATIVE:
338
+ info.mode = GenerationMode.NEGATIVE
339
+
340
+ container = self._template.setdefault(component_name, {})
341
+ container[name] = value.value
342
+
343
+ def set_body(self, body: coverage.GeneratedValue, media_type: str) -> None:
344
+ self._template["body"] = body.value
345
+ self._template["media_type"] = media_type
346
+ self._components[ComponentKind.BODY] = ComponentInfo(mode=body.generation_mode)
347
+
348
+ def _serialize(self, kwargs: dict[str, Any]) -> dict[str, Any]:
349
+ from schemathesis.specs.openapi._hypothesis import quote_all
350
+
351
+ output = {}
352
+ for container_name, value in kwargs.items():
353
+ serializer = self._serializers.get(container_name)
354
+ if container_name in ("headers", "cookies") and isinstance(value, dict):
355
+ value = _stringify_value(value, container_name)
356
+ if serializer is not None:
357
+ value = serializer(value)
358
+ if container_name == "query" and isinstance(value, dict):
359
+ value = _stringify_value(value, container_name)
360
+ if container_name == "path_parameters" and isinstance(value, dict):
361
+ value = _stringify_value(quote_all(value), container_name)
362
+ output[container_name] = value
363
+ return output
364
+
365
+ def unmodified(self) -> TemplateValue:
366
+ kwargs = self._template.copy()
367
+ kwargs = self._serialize(kwargs)
368
+ return TemplateValue(kwargs=kwargs, components=self._components.copy())
369
+
370
+ def with_body(self, *, media_type: str, value: coverage.GeneratedValue) -> TemplateValue:
371
+ kwargs = {**self._template, "media_type": media_type, "body": value.value}
372
+ kwargs = self._serialize(kwargs)
373
+ components = {**self._components, ComponentKind.BODY: ComponentInfo(mode=value.generation_mode)}
374
+ return TemplateValue(kwargs=kwargs, components=components)
375
+
376
+ def with_parameter(self, *, location: str, name: str, value: coverage.GeneratedValue) -> TemplateValue:
377
+ from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
378
+
379
+ container_name = LOCATION_TO_CONTAINER[location]
380
+ container = self._template[container_name]
381
+ return self.with_container(
382
+ container_name=container_name, value={**container, name: value.value}, generation_mode=value.generation_mode
383
+ )
384
+
385
+ def with_container(self, *, container_name: str, value: Any, generation_mode: GenerationMode) -> TemplateValue:
386
+ kwargs = {**self._template, container_name: value}
387
+ components = {**self._components, ComponentKind(container_name): ComponentInfo(mode=generation_mode)}
388
+ kwargs = self._serialize(kwargs)
389
+ return TemplateValue(kwargs=kwargs, components=components)
390
+
391
+
392
+ @dataclass
393
+ class TemplateValue:
394
+ kwargs: dict[str, Any]
395
+ components: dict[ComponentKind, ComponentInfo]
396
+ __slots__ = ("kwargs", "components")
397
+
398
+
399
+ def _stringify_value(val: Any, container_name: str) -> Any:
400
+ if val is None:
401
+ return "null"
402
+ if val is True:
403
+ return "true"
404
+ if val is False:
405
+ return "false"
406
+ if isinstance(val, (int, float)):
407
+ return str(val)
408
+ if isinstance(val, list):
409
+ if container_name == "query":
410
+ # Having a list here ensures there will be multiple query parameters wit the same name
411
+ return [_stringify_value(item, container_name) for item in val]
412
+ # use comma-separated values style for arrays
413
+ return ",".join(str(_stringify_value(sub, container_name)) for sub in val)
414
+ if isinstance(val, dict):
415
+ return {key: _stringify_value(sub, container_name) for key, sub in val.items()}
416
+ return val
417
+
418
+
419
+ def _iter_coverage_cases(
420
+ operation: APIOperation,
421
+ generation_modes: list[GenerationMode],
422
+ generate_duplicate_query_parameters: bool,
423
+ unexpected_methods: set[str] | None = None,
424
+ ) -> Generator[Case, None, None]:
425
+ from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
426
+ from schemathesis.specs.openapi.examples import find_in_responses, find_matching_in_responses
427
+ from schemathesis.specs.openapi.serialization import get_serializers_for_operation
428
+
429
+ generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
430
+ serializers = get_serializers_for_operation(operation)
431
+ template = Template(serializers)
432
+
433
+ instant = Instant()
434
+ responses = find_in_responses(operation)
435
+ # NOTE: The HEAD method is excluded
436
+ unexpected_methods = unexpected_methods or {"get", "put", "post", "delete", "options", "patch", "trace"}
437
+
438
+ seen_negative = coverage.HashSet()
439
+ seen_positive = coverage.HashSet()
440
+
441
+ for parameter in operation.iter_parameters():
442
+ location = parameter.location
443
+ name = parameter.name
444
+ schema = parameter.as_json_schema(operation, update_quantifiers=False)
445
+ for value in find_matching_in_responses(responses, parameter.name):
446
+ schema.setdefault("examples", []).append(value)
447
+ gen = coverage.cover_schema_iter(
448
+ coverage.CoverageContext(location=location, generation_modes=generation_modes), schema
449
+ )
450
+ value = next(gen, NOT_SET)
451
+ if isinstance(value, NotSet):
452
+ continue
453
+ template.add_parameter(location, name, value)
454
+ generators[(location, name)] = gen
455
+ template_time = instant.elapsed
456
+ if operation.body:
457
+ for body in operation.body:
458
+ instant = Instant()
459
+ schema = body.as_json_schema(operation, update_quantifiers=False)
460
+ # Definition could be a list for Open API 2.0
461
+ definition = body.definition if isinstance(body.definition, dict) else {}
462
+ examples = [example["value"] for example in definition.get("examples", {}).values() if "value" in example]
463
+ if examples:
464
+ schema.setdefault("examples", []).extend(examples)
465
+ gen = coverage.cover_schema_iter(
466
+ coverage.CoverageContext(location="body", generation_modes=generation_modes), schema
467
+ )
468
+ value = next(gen, NOT_SET)
469
+ if isinstance(value, NotSet):
470
+ continue
471
+ elapsed = instant.elapsed
472
+ if "body" not in template:
473
+ template_time += elapsed
474
+ template.set_body(value, body.media_type)
475
+ data = template.with_body(value=value, media_type=body.media_type)
476
+ yield operation.Case(
477
+ **data.kwargs,
478
+ _meta=CaseMetadata(
479
+ generation=GenerationInfo(
480
+ time=elapsed,
481
+ mode=value.generation_mode,
482
+ ),
483
+ components=data.components,
484
+ phase=PhaseInfo.coverage(
485
+ description=value.description,
486
+ location=value.location,
487
+ parameter=body.media_type,
488
+ parameter_location="body",
489
+ ),
490
+ ),
491
+ )
492
+ iterator = iter(gen)
493
+ while True:
494
+ instant = Instant()
495
+ try:
496
+ next_value = next(iterator)
497
+ data = template.with_body(value=next_value, media_type=body.media_type)
498
+ yield operation.Case(
499
+ **data.kwargs,
500
+ _meta=CaseMetadata(
501
+ generation=GenerationInfo(
502
+ time=instant.elapsed,
503
+ mode=next_value.generation_mode,
504
+ ),
505
+ components=data.components,
506
+ phase=PhaseInfo.coverage(
507
+ description=next_value.description,
508
+ location=next_value.location,
509
+ parameter=body.media_type,
510
+ parameter_location="body",
511
+ ),
512
+ ),
513
+ )
514
+ except StopIteration:
515
+ break
516
+ elif GenerationMode.POSITIVE in generation_modes:
517
+ data = template.unmodified()
518
+ seen_positive.insert(data.kwargs)
519
+ yield operation.Case(
520
+ **data.kwargs,
521
+ _meta=CaseMetadata(
522
+ generation=GenerationInfo(
523
+ time=template_time,
524
+ mode=GenerationMode.POSITIVE,
525
+ ),
526
+ components=data.components,
527
+ phase=PhaseInfo.coverage(description="Default positive test case"),
528
+ ),
529
+ )
530
+
531
+ for (location, name), gen in generators.items():
532
+ iterator = iter(gen)
533
+ while True:
534
+ instant = Instant()
535
+ try:
536
+ value = next(iterator)
537
+ data = template.with_parameter(location=location, name=name, value=value)
538
+ except StopIteration:
539
+ break
540
+
541
+ if value.generation_mode == GenerationMode.NEGATIVE:
542
+ seen_negative.insert(data.kwargs)
543
+ elif value.generation_mode == GenerationMode.POSITIVE and not seen_positive.insert(data.kwargs):
544
+ # Was already generated before
545
+ continue
546
+
547
+ yield operation.Case(
548
+ **data.kwargs,
549
+ _meta=CaseMetadata(
550
+ generation=GenerationInfo(time=instant.elapsed, mode=value.generation_mode),
551
+ components=data.components,
552
+ phase=PhaseInfo.coverage(
553
+ description=value.description,
554
+ location=value.location,
555
+ parameter=name,
556
+ parameter_location=location,
557
+ ),
558
+ ),
559
+ )
560
+ if GenerationMode.NEGATIVE in generation_modes:
561
+ # Generate HTTP methods that are not specified in the spec
562
+ methods = unexpected_methods - set(operation.schema[operation.path])
563
+ for method in sorted(methods):
564
+ instant = Instant()
565
+ data = template.unmodified()
566
+ yield operation.Case(
567
+ **data.kwargs,
568
+ method=method.upper(),
569
+ _meta=CaseMetadata(
570
+ generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
571
+ components=data.components,
572
+ phase=PhaseInfo.coverage(description=f"Unspecified HTTP method: {method.upper()}"),
573
+ ),
574
+ )
575
+ # Generate duplicate query parameters
576
+ if generate_duplicate_query_parameters and operation.query:
577
+ container = template["query"]
578
+ for parameter in operation.query:
579
+ instant = Instant()
580
+ # Could be absent if value schema can't be negated
581
+ # I.e. contains just `default` value without any other keywords
582
+ value = container.get(parameter.name, NOT_SET)
583
+ if value is not NOT_SET:
584
+ data = template.with_container(
585
+ container_name="query",
586
+ value={**container, parameter.name: [value, value]},
587
+ generation_mode=GenerationMode.NEGATIVE,
588
+ )
589
+ yield operation.Case(
590
+ **data.kwargs,
591
+ _meta=CaseMetadata(
592
+ generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
593
+ components=data.components,
594
+ phase=PhaseInfo.coverage(
595
+ description=f"Duplicate `{parameter.name}` query parameter",
596
+ parameter=parameter.name,
597
+ parameter_location="query",
598
+ ),
599
+ ),
600
+ )
601
+ # Generate missing required parameters
602
+ for parameter in operation.iter_parameters():
603
+ if parameter.is_required and parameter.location != "path":
604
+ instant = Instant()
605
+ name = parameter.name
606
+ location = parameter.location
607
+ container_name = LOCATION_TO_CONTAINER[location]
608
+ container = template[container_name]
609
+ data = template.with_container(
610
+ container_name=container_name,
611
+ value={k: v for k, v in container.items() if k != name},
612
+ generation_mode=GenerationMode.NEGATIVE,
613
+ )
614
+ yield operation.Case(
615
+ **data.kwargs,
616
+ _meta=CaseMetadata(
617
+ generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
618
+ components=data.components,
619
+ phase=PhaseInfo.coverage(
620
+ description=f"Missing `{name}` at {location}",
621
+ parameter=name,
622
+ parameter_location=location,
623
+ ),
624
+ ),
625
+ )
626
+ # Generate combinations for each location
627
+ for location, parameter_set in [
628
+ ("query", operation.query),
629
+ ("header", operation.headers),
630
+ ("cookie", operation.cookies),
631
+ ]:
632
+ if not parameter_set:
633
+ continue
634
+
635
+ container_name = LOCATION_TO_CONTAINER[location]
636
+ base_container = template.get(container_name, {})
637
+
638
+ # Get required and optional parameters
639
+ required = {p.name for p in parameter_set if p.is_required}
640
+ all_params = {p.name for p in parameter_set}
641
+ optional = sorted(all_params - required)
642
+
643
+ # Helper function to create and yield a case
644
+ def make_case(
645
+ container_values: dict,
646
+ description: str,
647
+ _location: str,
648
+ _container_name: str,
649
+ _parameter: str | None,
650
+ _generation_mode: GenerationMode,
651
+ _instant: Instant,
652
+ ) -> Case:
653
+ data = template.with_container(
654
+ container_name=_container_name, value=container_values, generation_mode=_generation_mode
655
+ )
656
+ return operation.Case(
657
+ **data.kwargs,
658
+ _meta=CaseMetadata(
659
+ generation=GenerationInfo(
660
+ time=_instant.elapsed,
661
+ mode=_generation_mode,
662
+ ),
663
+ components=data.components,
664
+ phase=PhaseInfo.coverage(
665
+ description=description,
666
+ parameter=_parameter,
667
+ parameter_location=_location,
668
+ ),
669
+ ),
670
+ )
671
+
672
+ def _combination_schema(
673
+ combination: dict[str, Any], _required: set[str], _parameter_set: ParameterSet
674
+ ) -> dict[str, Any]:
675
+ return {
676
+ "properties": {
677
+ parameter.name: parameter.as_json_schema(operation)
678
+ for parameter in _parameter_set
679
+ if parameter.name in combination
680
+ },
681
+ "required": list(_required),
682
+ "additionalProperties": False,
683
+ }
684
+
685
+ def _yield_negative(
686
+ subschema: dict[str, Any], _location: str, _container_name: str
687
+ ) -> Generator[Case, None, None]:
688
+ iterator = iter(
689
+ coverage.cover_schema_iter(
690
+ coverage.CoverageContext(location=_location, generation_modes=[GenerationMode.NEGATIVE]),
691
+ subschema,
692
+ )
693
+ )
694
+ while True:
695
+ instant = Instant()
696
+ try:
697
+ more = next(iterator)
698
+ yield make_case(
699
+ more.value,
700
+ more.description,
701
+ _location,
702
+ _container_name,
703
+ more.parameter,
704
+ GenerationMode.NEGATIVE,
705
+ instant,
706
+ )
707
+ except StopIteration:
708
+ break
709
+
710
+ # 1. Generate only required properties
711
+ if required and all_params != required:
712
+ only_required = {k: v for k, v in base_container.items() if k in required}
713
+ if GenerationMode.POSITIVE in generation_modes:
714
+ yield make_case(
715
+ only_required,
716
+ "Only required properties",
717
+ location,
718
+ container_name,
719
+ None,
720
+ GenerationMode.POSITIVE,
721
+ Instant(),
722
+ )
723
+ if GenerationMode.NEGATIVE in generation_modes:
724
+ subschema = _combination_schema(only_required, required, parameter_set)
725
+ for case in _yield_negative(subschema, location, container_name):
726
+ kwargs = _case_to_kwargs(case)
727
+ if not seen_negative.insert(kwargs):
728
+ continue
729
+ assert case.meta is not None
730
+ assert isinstance(case.meta.phase.data, CoveragePhaseData)
731
+ # Already generated in one of the blocks above
732
+ if location != "path" and not case.meta.phase.data.description.startswith(
733
+ "Missing required property"
734
+ ):
735
+ yield case
736
+
737
+ # 2. Generate combinations with required properties and one optional property
738
+ for opt_param in optional:
739
+ combo = {k: v for k, v in base_container.items() if k in required or k == opt_param}
740
+ if combo != base_container and GenerationMode.POSITIVE in generation_modes:
741
+ yield make_case(
742
+ combo,
743
+ f"All required properties and optional '{opt_param}'",
744
+ location,
745
+ container_name,
746
+ None,
747
+ GenerationMode.POSITIVE,
748
+ Instant(),
749
+ )
750
+ if GenerationMode.NEGATIVE in generation_modes:
751
+ subschema = _combination_schema(combo, required, parameter_set)
752
+ for case in _yield_negative(subschema, location, container_name):
753
+ assert case.meta is not None
754
+ assert isinstance(case.meta.phase.data, CoveragePhaseData)
755
+ # Already generated in one of the blocks above
756
+ if location != "path" and not case.meta.phase.data.description.startswith(
757
+ "Missing required property"
758
+ ):
759
+ yield case
760
+
761
+ # 3. Generate one combination for each size from 2 to N-1 of optional parameters
762
+ if len(optional) > 1 and GenerationMode.POSITIVE in generation_modes:
763
+ for size in range(2, len(optional)):
764
+ for combination in combinations(optional, size):
765
+ combo = {k: v for k, v in base_container.items() if k in required or k in combination}
766
+ if combo != base_container:
767
+ yield make_case(
768
+ combo,
769
+ f"All required and {size} optional properties",
770
+ location,
771
+ container_name,
772
+ None,
773
+ GenerationMode.POSITIVE,
774
+ Instant(),
775
+ )
776
+
777
+
778
+ def _case_to_kwargs(case: Case) -> dict:
779
+ from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
780
+
781
+ kwargs = {}
782
+ for container_name in LOCATION_TO_CONTAINER.values():
783
+ value = getattr(case, container_name)
784
+ if isinstance(value, CaseInsensitiveDict) and value:
785
+ kwargs[container_name] = dict(value)
786
+ elif value and value is not NOT_SET:
787
+ kwargs[container_name] = value
788
+ return kwargs
789
+
790
+
791
+ def find_invalid_headers(headers: Mapping) -> Generator[tuple[str, str], None, None]:
792
+ for name, value in headers.items():
793
+ if not is_latin_1_encodable(value) or has_invalid_characters(name, value):
794
+ yield name, value
795
+
796
+
797
+ UnsatisfiableExampleMark = Mark[Unsatisfiable](attr_name="unsatisfiable_example")
798
+ NonSerializableMark = Mark[SerializationNotPossible](attr_name="non_serializable")
799
+ InvalidRegexMark = Mark[SchemaError](attr_name="invalid_regex")
800
+ InvalidHeadersExampleMark = Mark[dict[str, str]](attr_name="invalid_example_header")