schemathesis 3.25.5__py3-none-any.whl → 4.0.0a1__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 (221) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +102 -82
  3. schemathesis/checks.py +126 -46
  4. schemathesis/cli/__init__.py +11 -1766
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +37 -0
  7. schemathesis/cli/commands/run/__init__.py +662 -0
  8. schemathesis/cli/commands/run/checks.py +80 -0
  9. schemathesis/cli/commands/run/context.py +117 -0
  10. schemathesis/cli/commands/run/events.py +35 -0
  11. schemathesis/cli/commands/run/executor.py +138 -0
  12. schemathesis/cli/commands/run/filters.py +194 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +18 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  17. schemathesis/cli/commands/run/handlers/output.py +746 -0
  18. schemathesis/cli/commands/run/hypothesis.py +105 -0
  19. schemathesis/cli/commands/run/loaders.py +129 -0
  20. schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
  21. schemathesis/cli/constants.py +5 -52
  22. schemathesis/cli/core.py +17 -0
  23. schemathesis/cli/ext/fs.py +14 -0
  24. schemathesis/cli/ext/groups.py +55 -0
  25. schemathesis/cli/{options.py → ext/options.py} +39 -10
  26. schemathesis/cli/hooks.py +36 -0
  27. schemathesis/contrib/__init__.py +1 -3
  28. schemathesis/contrib/openapi/__init__.py +1 -3
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
  30. schemathesis/core/__init__.py +58 -0
  31. schemathesis/core/compat.py +25 -0
  32. schemathesis/core/control.py +2 -0
  33. schemathesis/core/curl.py +58 -0
  34. schemathesis/core/deserialization.py +65 -0
  35. schemathesis/core/errors.py +370 -0
  36. schemathesis/core/failures.py +285 -0
  37. schemathesis/core/fs.py +19 -0
  38. schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
  39. schemathesis/core/loaders.py +104 -0
  40. schemathesis/core/marks.py +66 -0
  41. schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
  42. schemathesis/core/output/__init__.py +69 -0
  43. schemathesis/core/output/sanitization.py +197 -0
  44. schemathesis/core/rate_limit.py +60 -0
  45. schemathesis/core/registries.py +31 -0
  46. schemathesis/{internal → core}/result.py +1 -1
  47. schemathesis/core/transforms.py +113 -0
  48. schemathesis/core/transport.py +108 -0
  49. schemathesis/core/validation.py +38 -0
  50. schemathesis/core/version.py +7 -0
  51. schemathesis/engine/__init__.py +30 -0
  52. schemathesis/engine/config.py +59 -0
  53. schemathesis/engine/context.py +119 -0
  54. schemathesis/engine/control.py +36 -0
  55. schemathesis/engine/core.py +157 -0
  56. schemathesis/engine/errors.py +394 -0
  57. schemathesis/engine/events.py +337 -0
  58. schemathesis/engine/phases/__init__.py +66 -0
  59. schemathesis/{cli → engine/phases}/probes.py +63 -70
  60. schemathesis/engine/phases/stateful/__init__.py +65 -0
  61. schemathesis/engine/phases/stateful/_executor.py +326 -0
  62. schemathesis/engine/phases/stateful/context.py +85 -0
  63. schemathesis/engine/phases/unit/__init__.py +174 -0
  64. schemathesis/engine/phases/unit/_executor.py +321 -0
  65. schemathesis/engine/phases/unit/_pool.py +74 -0
  66. schemathesis/engine/recorder.py +241 -0
  67. schemathesis/errors.py +31 -0
  68. schemathesis/experimental/__init__.py +18 -14
  69. schemathesis/filters.py +103 -14
  70. schemathesis/generation/__init__.py +21 -37
  71. schemathesis/generation/case.py +190 -0
  72. schemathesis/generation/coverage.py +931 -0
  73. schemathesis/generation/hypothesis/__init__.py +30 -0
  74. schemathesis/generation/hypothesis/builder.py +585 -0
  75. schemathesis/generation/hypothesis/examples.py +50 -0
  76. schemathesis/generation/hypothesis/given.py +66 -0
  77. schemathesis/generation/hypothesis/reporting.py +14 -0
  78. schemathesis/generation/hypothesis/strategies.py +16 -0
  79. schemathesis/generation/meta.py +115 -0
  80. schemathesis/generation/modes.py +28 -0
  81. schemathesis/generation/overrides.py +96 -0
  82. schemathesis/generation/stateful/__init__.py +20 -0
  83. schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
  84. schemathesis/generation/targets.py +69 -0
  85. schemathesis/graphql/__init__.py +15 -0
  86. schemathesis/graphql/checks.py +115 -0
  87. schemathesis/graphql/loaders.py +131 -0
  88. schemathesis/hooks.py +99 -67
  89. schemathesis/openapi/__init__.py +13 -0
  90. schemathesis/openapi/checks.py +412 -0
  91. schemathesis/openapi/generation/__init__.py +0 -0
  92. schemathesis/openapi/generation/filters.py +63 -0
  93. schemathesis/openapi/loaders.py +178 -0
  94. schemathesis/pytest/__init__.py +5 -0
  95. schemathesis/pytest/control_flow.py +7 -0
  96. schemathesis/pytest/lazy.py +273 -0
  97. schemathesis/pytest/loaders.py +12 -0
  98. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
  99. schemathesis/python/__init__.py +0 -0
  100. schemathesis/python/asgi.py +12 -0
  101. schemathesis/python/wsgi.py +12 -0
  102. schemathesis/schemas.py +537 -261
  103. schemathesis/specs/graphql/__init__.py +0 -1
  104. schemathesis/specs/graphql/_cache.py +25 -0
  105. schemathesis/specs/graphql/nodes.py +1 -0
  106. schemathesis/specs/graphql/scalars.py +7 -5
  107. schemathesis/specs/graphql/schemas.py +215 -187
  108. schemathesis/specs/graphql/validation.py +11 -18
  109. schemathesis/specs/openapi/__init__.py +7 -1
  110. schemathesis/specs/openapi/_cache.py +122 -0
  111. schemathesis/specs/openapi/_hypothesis.py +146 -165
  112. schemathesis/specs/openapi/checks.py +565 -67
  113. schemathesis/specs/openapi/converter.py +33 -6
  114. schemathesis/specs/openapi/definitions.py +11 -18
  115. schemathesis/specs/openapi/examples.py +153 -39
  116. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  117. schemathesis/specs/openapi/expressions/context.py +4 -6
  118. schemathesis/specs/openapi/expressions/extractors.py +23 -0
  119. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  120. schemathesis/specs/openapi/expressions/nodes.py +38 -14
  121. schemathesis/specs/openapi/expressions/parser.py +26 -5
  122. schemathesis/specs/openapi/formats.py +45 -0
  123. schemathesis/specs/openapi/links.py +65 -165
  124. schemathesis/specs/openapi/media_types.py +32 -0
  125. schemathesis/specs/openapi/negative/__init__.py +7 -3
  126. schemathesis/specs/openapi/negative/mutations.py +24 -8
  127. schemathesis/specs/openapi/parameters.py +46 -30
  128. schemathesis/specs/openapi/patterns.py +137 -0
  129. schemathesis/specs/openapi/references.py +47 -57
  130. schemathesis/specs/openapi/schemas.py +483 -367
  131. schemathesis/specs/openapi/security.py +25 -7
  132. schemathesis/specs/openapi/serialization.py +11 -6
  133. schemathesis/specs/openapi/stateful/__init__.py +185 -73
  134. schemathesis/specs/openapi/utils.py +6 -1
  135. schemathesis/transport/__init__.py +104 -0
  136. schemathesis/transport/asgi.py +26 -0
  137. schemathesis/transport/prepare.py +99 -0
  138. schemathesis/transport/requests.py +221 -0
  139. schemathesis/{_xml.py → transport/serialization.py} +143 -28
  140. schemathesis/transport/wsgi.py +165 -0
  141. schemathesis-4.0.0a1.dist-info/METADATA +297 -0
  142. schemathesis-4.0.0a1.dist-info/RECORD +152 -0
  143. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
  144. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
  145. schemathesis/_compat.py +0 -74
  146. schemathesis/_dependency_versions.py +0 -17
  147. schemathesis/_hypothesis.py +0 -246
  148. schemathesis/_override.py +0 -49
  149. schemathesis/cli/cassettes.py +0 -375
  150. schemathesis/cli/context.py +0 -55
  151. schemathesis/cli/debug.py +0 -26
  152. schemathesis/cli/handlers.py +0 -16
  153. schemathesis/cli/junitxml.py +0 -43
  154. schemathesis/cli/output/__init__.py +0 -1
  155. schemathesis/cli/output/default.py +0 -765
  156. schemathesis/cli/output/short.py +0 -40
  157. schemathesis/cli/sanitization.py +0 -20
  158. schemathesis/code_samples.py +0 -149
  159. schemathesis/constants.py +0 -55
  160. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  161. schemathesis/contrib/openapi/formats/uuid.py +0 -15
  162. schemathesis/contrib/unique_data.py +0 -41
  163. schemathesis/exceptions.py +0 -560
  164. schemathesis/extra/_aiohttp.py +0 -27
  165. schemathesis/extra/_flask.py +0 -10
  166. schemathesis/extra/_server.py +0 -17
  167. schemathesis/failures.py +0 -209
  168. schemathesis/fixups/__init__.py +0 -36
  169. schemathesis/fixups/fast_api.py +0 -41
  170. schemathesis/fixups/utf8_bom.py +0 -29
  171. schemathesis/graphql.py +0 -4
  172. schemathesis/internal/__init__.py +0 -7
  173. schemathesis/internal/copy.py +0 -13
  174. schemathesis/internal/datetime.py +0 -5
  175. schemathesis/internal/deprecation.py +0 -34
  176. schemathesis/internal/jsonschema.py +0 -35
  177. schemathesis/internal/transformation.py +0 -15
  178. schemathesis/internal/validation.py +0 -34
  179. schemathesis/lazy.py +0 -361
  180. schemathesis/loaders.py +0 -120
  181. schemathesis/models.py +0 -1231
  182. schemathesis/parameters.py +0 -86
  183. schemathesis/runner/__init__.py +0 -555
  184. schemathesis/runner/events.py +0 -309
  185. schemathesis/runner/impl/__init__.py +0 -3
  186. schemathesis/runner/impl/core.py +0 -986
  187. schemathesis/runner/impl/solo.py +0 -90
  188. schemathesis/runner/impl/threadpool.py +0 -400
  189. schemathesis/runner/serialization.py +0 -411
  190. schemathesis/sanitization.py +0 -248
  191. schemathesis/serializers.py +0 -315
  192. schemathesis/service/__init__.py +0 -18
  193. schemathesis/service/auth.py +0 -11
  194. schemathesis/service/ci.py +0 -201
  195. schemathesis/service/client.py +0 -100
  196. schemathesis/service/constants.py +0 -38
  197. schemathesis/service/events.py +0 -57
  198. schemathesis/service/hosts.py +0 -107
  199. schemathesis/service/metadata.py +0 -46
  200. schemathesis/service/models.py +0 -49
  201. schemathesis/service/report.py +0 -255
  202. schemathesis/service/serialization.py +0 -184
  203. schemathesis/service/usage.py +0 -65
  204. schemathesis/specs/graphql/loaders.py +0 -344
  205. schemathesis/specs/openapi/filters.py +0 -49
  206. schemathesis/specs/openapi/loaders.py +0 -667
  207. schemathesis/specs/openapi/stateful/links.py +0 -92
  208. schemathesis/specs/openapi/validation.py +0 -25
  209. schemathesis/stateful/__init__.py +0 -133
  210. schemathesis/targets.py +0 -45
  211. schemathesis/throttling.py +0 -41
  212. schemathesis/transports/__init__.py +0 -5
  213. schemathesis/transports/auth.py +0 -15
  214. schemathesis/transports/headers.py +0 -35
  215. schemathesis/transports/responses.py +0 -52
  216. schemathesis/types.py +0 -35
  217. schemathesis/utils.py +0 -169
  218. schemathesis-3.25.5.dist-info/METADATA +0 -356
  219. schemathesis-3.25.5.dist-info/RECORD +0 -134
  220. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  221. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,30 @@
1
+ from typing import Any
2
+
3
+ # Maximum test running time
4
+ DEFAULT_DEADLINE = 15000
5
+
6
+
7
+ def setup() -> None:
8
+ from hypothesis.internal.entropy import deterministic_PRNG
9
+ from hypothesis.internal.reflection import is_first_param_referenced_in_function
10
+ from hypothesis.strategies._internal import core
11
+ from hypothesis_jsonschema import _from_schema, _resolve
12
+
13
+ from schemathesis.core.transforms import deepclone
14
+
15
+ # Forcefully initializes Hypothesis' global PRNG to avoid races that initialize it
16
+ # if e.g. Schemathesis CLI is used with multiple workers
17
+ with deterministic_PRNG():
18
+ pass
19
+
20
+ # A set of performance-related patches
21
+
22
+ # This one is used a lot, and under the hood it re-parses the AST of the same function
23
+ def _is_first_param_referenced_in_function(f: Any) -> bool:
24
+ if f.__name__ == "from_object_schema" and f.__module__ == "hypothesis_jsonschema._from_schema":
25
+ return True
26
+ return is_first_param_referenced_in_function(f)
27
+
28
+ core.is_first_param_referenced_in_function = _is_first_param_referenced_in_function # type: ignore
29
+ _resolve.deepcopy = deepclone # type: ignore
30
+ _from_schema.deepcopy = deepclone # type: ignore
@@ -0,0 +1,585 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass, field
5
+ from functools import wraps
6
+ from itertools import combinations
7
+ from time import perf_counter
8
+ from typing import Any, Callable, Generator, Mapping
9
+
10
+ import hypothesis
11
+ from hypothesis import Phase
12
+ from hypothesis import strategies as st
13
+ from hypothesis.errors import Unsatisfiable
14
+ from jsonschema.exceptions import SchemaError
15
+
16
+ from schemathesis.auths import AuthStorageMark
17
+ from schemathesis.core import NOT_SET, NotSet, media_types
18
+ from schemathesis.core.errors import InvalidSchema, SerializationNotPossible
19
+ from schemathesis.core.marks import Mark
20
+ from schemathesis.core.result import Ok, Result
21
+ from schemathesis.core.transport import prepare_urlencoded
22
+ from schemathesis.core.validation import has_invalid_characters, is_latin_1_encodable
23
+ from schemathesis.experimental import COVERAGE_PHASE
24
+ from schemathesis.generation import GenerationConfig, GenerationMode, coverage
25
+ from schemathesis.generation.case import Case
26
+ from schemathesis.generation.hypothesis import DEFAULT_DEADLINE, examples, setup, strategies
27
+ from schemathesis.generation.hypothesis.given import GivenInput
28
+ from schemathesis.generation.meta import CaseMetadata, CoveragePhaseData, GenerationInfo, PhaseInfo
29
+ from schemathesis.hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookDispatcherMark
30
+ from schemathesis.schemas import APIOperation, BaseSchema, ParameterSet
31
+
32
+ setup()
33
+
34
+
35
+ def get_all_tests(
36
+ *,
37
+ schema: BaseSchema,
38
+ test_func: Callable,
39
+ generation_config: GenerationConfig,
40
+ settings: hypothesis.settings | None = None,
41
+ seed: int | None = None,
42
+ as_strategy_kwargs: Callable[[APIOperation], dict[str, Any]] | None = None,
43
+ given_kwargs: dict[str, GivenInput] | None = None,
44
+ ) -> Generator[Result[tuple[APIOperation, Callable], InvalidSchema], None, None]:
45
+ """Generate all operations and Hypothesis tests for them."""
46
+ for result in schema.get_all_operations(generation_config=generation_config):
47
+ if isinstance(result, Ok):
48
+ operation = result.ok()
49
+ if callable(as_strategy_kwargs):
50
+ _as_strategy_kwargs = as_strategy_kwargs(operation)
51
+ else:
52
+ _as_strategy_kwargs = {}
53
+ test = create_test(
54
+ operation=operation,
55
+ test_func=test_func,
56
+ config=HypothesisTestConfig(
57
+ settings=settings,
58
+ seed=seed,
59
+ generation=generation_config,
60
+ as_strategy_kwargs=_as_strategy_kwargs,
61
+ given_kwargs=given_kwargs or {},
62
+ ),
63
+ )
64
+ yield Ok((operation, test))
65
+ else:
66
+ yield result
67
+
68
+
69
+ @dataclass
70
+ class HypothesisTestConfig:
71
+ generation: GenerationConfig
72
+ settings: hypothesis.settings | None = None
73
+ seed: int | None = None
74
+ as_strategy_kwargs: dict[str, Any] = field(default_factory=dict)
75
+ given_args: tuple[GivenInput, ...] = ()
76
+ given_kwargs: dict[str, GivenInput] = field(default_factory=dict)
77
+
78
+
79
+ def create_test(
80
+ *,
81
+ operation: APIOperation,
82
+ test_func: Callable,
83
+ config: HypothesisTestConfig,
84
+ ) -> Callable:
85
+ """Create a Hypothesis test."""
86
+ hook_dispatcher = HookDispatcherMark.get(test_func)
87
+ auth_storage = AuthStorageMark.get(test_func)
88
+
89
+ strategy_kwargs = {
90
+ "hooks": hook_dispatcher,
91
+ "auth_storage": auth_storage,
92
+ "generation_config": config.generation,
93
+ **config.as_strategy_kwargs,
94
+ }
95
+ strategy = strategies.combine(
96
+ [operation.as_strategy(generation_mode=mode, **strategy_kwargs) for mode in config.generation.modes]
97
+ )
98
+
99
+ hypothesis_test = create_base_test(
100
+ test_function=test_func,
101
+ strategy=strategy,
102
+ args=config.given_args,
103
+ kwargs=config.given_kwargs,
104
+ )
105
+
106
+ if config.seed is not None:
107
+ hypothesis_test = hypothesis.seed(config.seed)(hypothesis_test)
108
+
109
+ default = hypothesis.settings.default
110
+ settings = getattr(hypothesis_test, SETTINGS_ATTRIBUTE_NAME, None)
111
+ assert settings is not None
112
+
113
+ if settings.deadline == default.deadline:
114
+ settings = hypothesis.settings(settings, deadline=DEFAULT_DEADLINE)
115
+
116
+ if config.settings is not None:
117
+ # Merge the user-provided settings with the current ones
118
+ settings = hypothesis.settings(
119
+ settings,
120
+ **{item: value for item, value in config.settings.__dict__.items() if value != getattr(default, item)},
121
+ )
122
+
123
+ if Phase.explain in settings.phases:
124
+ phases = tuple(phase for phase in settings.phases if phase != Phase.explain)
125
+ settings = hypothesis.settings(settings, phases=phases)
126
+
127
+ # Add examples if explicit phase is enabled
128
+ if Phase.explicit in settings.phases:
129
+ hypothesis_test = add_examples(hypothesis_test, operation, hook_dispatcher=hook_dispatcher, **strategy_kwargs)
130
+
131
+ if COVERAGE_PHASE.is_enabled:
132
+ # Ensure explicit phase is enabled if coverage is enabled
133
+ if Phase.explicit not in settings.phases:
134
+ phases = settings.phases + (Phase.explicit,)
135
+ settings = hypothesis.settings(settings, phases=phases)
136
+ hypothesis_test = add_coverage(hypothesis_test, operation, config.generation.modes)
137
+
138
+ setattr(hypothesis_test, SETTINGS_ATTRIBUTE_NAME, settings)
139
+
140
+ return hypothesis_test
141
+
142
+
143
+ SETTINGS_ATTRIBUTE_NAME = "_hypothesis_internal_use_settings"
144
+
145
+
146
+ def create_base_test(
147
+ *,
148
+ test_function: Callable,
149
+ strategy: st.SearchStrategy,
150
+ args: tuple[GivenInput, ...],
151
+ kwargs: dict[str, GivenInput],
152
+ ) -> Callable:
153
+ """Create the basic Hypothesis test with the given strategy."""
154
+
155
+ @wraps(test_function)
156
+ def test_wrapper(*args: Any, **kwargs: Any) -> Any:
157
+ __tracebackhide__ = True
158
+ return test_function(*args, **kwargs)
159
+
160
+ return hypothesis.given(*args, **{**kwargs, "case": strategy})(test_wrapper)
161
+
162
+
163
+ def add_examples(
164
+ test: Callable, operation: APIOperation, hook_dispatcher: HookDispatcher | None = None, **kwargs: Any
165
+ ) -> Callable:
166
+ """Add examples to the Hypothesis test, if they are specified in the schema."""
167
+ from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
168
+
169
+ try:
170
+ result: list[Case] = [
171
+ examples.generate_one(strategy) for strategy in operation.get_strategies_from_examples(**kwargs)
172
+ ]
173
+ except (
174
+ InvalidSchema,
175
+ HypothesisRefResolutionError,
176
+ Unsatisfiable,
177
+ SerializationNotPossible,
178
+ SchemaError,
179
+ ) as exc:
180
+ result = []
181
+ if isinstance(exc, Unsatisfiable):
182
+ UnsatisfiableExampleMark.set(test, exc)
183
+ if isinstance(exc, SerializationNotPossible):
184
+ NonSerializableMark.set(test, exc)
185
+ if isinstance(exc, SchemaError):
186
+ InvalidRegexMark.set(test, exc)
187
+ context = HookContext(operation) # context should be passed here instead
188
+ GLOBAL_HOOK_DISPATCHER.dispatch("before_add_examples", context, result)
189
+ operation.schema.hooks.dispatch("before_add_examples", context, result)
190
+ if hook_dispatcher:
191
+ hook_dispatcher.dispatch("before_add_examples", context, result)
192
+ original_test = test
193
+ for example in result:
194
+ if example.headers is not None:
195
+ invalid_headers = dict(find_invalid_headers(example.headers))
196
+ if invalid_headers:
197
+ InvalidHeadersExampleMark.set(original_test, invalid_headers)
198
+ continue
199
+ adjust_urlencoded_payload(example)
200
+ test = hypothesis.example(case=example)(test)
201
+ return test
202
+
203
+
204
+ def adjust_urlencoded_payload(case: Case) -> None:
205
+ if case.media_type is not None:
206
+ try:
207
+ media_type = media_types.parse(case.media_type)
208
+ if media_type == ("application", "x-www-form-urlencoded"):
209
+ case.body = prepare_urlencoded(case.body)
210
+ except ValueError:
211
+ pass
212
+
213
+
214
+ def add_coverage(test: Callable, operation: APIOperation, generation_modes: list[GenerationMode]) -> Callable:
215
+ for example in _iter_coverage_cases(operation, generation_modes):
216
+ adjust_urlencoded_payload(example)
217
+ test = hypothesis.example(case=example)(test)
218
+ return test
219
+
220
+
221
+ class Instant:
222
+ __slots__ = ("start",)
223
+
224
+ def __init__(self) -> None:
225
+ self.start = perf_counter()
226
+
227
+ @property
228
+ def elapsed(self) -> float:
229
+ return perf_counter() - self.start
230
+
231
+
232
+ def _iter_coverage_cases(
233
+ operation: APIOperation, generation_modes: list[GenerationMode]
234
+ ) -> Generator[Case, None, None]:
235
+ from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
236
+ from schemathesis.specs.openapi.examples import find_in_responses, find_matching_in_responses
237
+
238
+ def _stringify_value(val: Any, location: str) -> str | list[str]:
239
+ if isinstance(val, list):
240
+ if location == "query":
241
+ # Having a list here ensures there will be multiple query parameters wit the same name
242
+ return [json.dumps(item) for item in val]
243
+ # use comma-separated values style for arrays
244
+ return ",".join(json.dumps(sub) for sub in val)
245
+ return json.dumps(val)
246
+
247
+ generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
248
+ template: dict[str, Any] = {}
249
+
250
+ instant = Instant()
251
+ responses = find_in_responses(operation)
252
+ for parameter in operation.iter_parameters():
253
+ location = parameter.location
254
+ name = parameter.name
255
+ schema = parameter.as_json_schema(operation, update_quantifiers=False)
256
+ for value in find_matching_in_responses(responses, parameter.name):
257
+ schema.setdefault("examples", []).append(value)
258
+ gen = coverage.cover_schema_iter(
259
+ coverage.CoverageContext(location=location, generation_modes=generation_modes), schema
260
+ )
261
+ value = next(gen, NOT_SET)
262
+ if isinstance(value, NotSet):
263
+ continue
264
+ container = template.setdefault(LOCATION_TO_CONTAINER[location], {})
265
+ if location in ("header", "cookie", "path", "query") and not isinstance(value.value, str):
266
+ container[name] = _stringify_value(value.value, location)
267
+ else:
268
+ container[name] = value.value
269
+ generators[(location, name)] = gen
270
+ template_time = instant.elapsed
271
+ if operation.body:
272
+ for body in operation.body:
273
+ instant = Instant()
274
+ schema = body.as_json_schema(operation, update_quantifiers=False)
275
+ # Definition could be a list for Open API 2.0
276
+ definition = body.definition if isinstance(body.definition, dict) else {}
277
+ examples = [example["value"] for example in definition.get("examples", {}).values() if "value" in example]
278
+ if examples:
279
+ schema.setdefault("examples", []).extend(examples)
280
+ gen = coverage.cover_schema_iter(
281
+ coverage.CoverageContext(location="body", generation_modes=generation_modes), schema
282
+ )
283
+ value = next(gen, NOT_SET)
284
+ if isinstance(value, NotSet):
285
+ continue
286
+ elapsed = instant.elapsed
287
+ if "body" not in template:
288
+ template_time += elapsed
289
+ template["body"] = value.value
290
+ template["media_type"] = body.media_type
291
+ yield operation.Case(
292
+ **{**template, "body": value.value, "media_type": body.media_type},
293
+ meta=CaseMetadata(
294
+ generation=GenerationInfo(
295
+ time=elapsed,
296
+ mode=value.generation_mode,
297
+ ),
298
+ components={},
299
+ phase=PhaseInfo.coverage(
300
+ description=value.description,
301
+ location=value.location,
302
+ parameter=body.media_type,
303
+ parameter_location="body",
304
+ ),
305
+ ),
306
+ )
307
+ iterator = iter(gen)
308
+ while True:
309
+ instant = Instant()
310
+ try:
311
+ next_value = next(iterator)
312
+ yield operation.Case(
313
+ **{**template, "body": next_value.value, "media_type": body.media_type},
314
+ meta=CaseMetadata(
315
+ generation=GenerationInfo(
316
+ time=instant.elapsed,
317
+ mode=value.generation_mode,
318
+ ),
319
+ components={},
320
+ phase=PhaseInfo.coverage(
321
+ description=next_value.description,
322
+ location=next_value.location,
323
+ parameter=body.media_type,
324
+ parameter_location="body",
325
+ ),
326
+ ),
327
+ )
328
+ except StopIteration:
329
+ break
330
+ elif GenerationMode.POSITIVE in generation_modes:
331
+ yield operation.Case(
332
+ **template,
333
+ meta=CaseMetadata(
334
+ generation=GenerationInfo(
335
+ time=template_time,
336
+ mode=GenerationMode.POSITIVE,
337
+ ),
338
+ components={},
339
+ phase=PhaseInfo.coverage(description="Default positive test case"),
340
+ ),
341
+ )
342
+
343
+ for (location, name), gen in generators.items():
344
+ container_name = LOCATION_TO_CONTAINER[location]
345
+ container = template[container_name]
346
+ iterator = iter(gen)
347
+ while True:
348
+ instant = Instant()
349
+ try:
350
+ value = next(iterator)
351
+ if location in ("header", "cookie", "path", "query") and not isinstance(value.value, str):
352
+ generated = _stringify_value(value.value, location)
353
+ else:
354
+ generated = value.value
355
+ except StopIteration:
356
+ break
357
+ yield operation.Case(
358
+ **{**template, container_name: {**container, name: generated}},
359
+ meta=CaseMetadata(
360
+ generation=GenerationInfo(time=instant.elapsed, mode=value.generation_mode),
361
+ components={},
362
+ phase=PhaseInfo.coverage(
363
+ description=value.description,
364
+ location=value.location,
365
+ parameter=name,
366
+ parameter_location=location,
367
+ ),
368
+ ),
369
+ )
370
+ if GenerationMode.NEGATIVE in generation_modes:
371
+ # Generate HTTP methods that are not specified in the spec
372
+ methods = {"get", "put", "post", "delete", "options", "patch", "trace"} - set(operation.schema[operation.path])
373
+ for method in sorted(methods):
374
+ instant = Instant()
375
+ yield operation.Case(
376
+ **template,
377
+ method=method.upper(),
378
+ meta=CaseMetadata(
379
+ generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
380
+ components={},
381
+ phase=PhaseInfo.coverage(description=f"Unspecified HTTP method: {method.upper()}"),
382
+ ),
383
+ )
384
+ # Generate duplicate query parameters
385
+ if operation.query:
386
+ container = template["query"]
387
+ for parameter in operation.query:
388
+ instant = Instant()
389
+ value = container[parameter.name]
390
+ yield operation.Case(
391
+ **{**template, "query": {**container, parameter.name: [value, value]}},
392
+ meta=CaseMetadata(
393
+ generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
394
+ components={},
395
+ phase=PhaseInfo.coverage(
396
+ description=f"Duplicate `{parameter.name}` query parameter",
397
+ parameter=parameter.name,
398
+ parameter_location="query",
399
+ ),
400
+ ),
401
+ )
402
+ # Generate missing required parameters
403
+ for parameter in operation.iter_parameters():
404
+ if parameter.is_required and parameter.location != "path":
405
+ instant = Instant()
406
+ name = parameter.name
407
+ location = parameter.location
408
+ container_name = LOCATION_TO_CONTAINER[location]
409
+ container = template[container_name]
410
+ yield operation.Case(
411
+ **{**template, container_name: {k: v for k, v in container.items() if k != name}},
412
+ meta=CaseMetadata(
413
+ generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
414
+ components={},
415
+ phase=PhaseInfo.coverage(
416
+ description=f"Missing `{name}` at {location}",
417
+ parameter=name,
418
+ parameter_location=location,
419
+ ),
420
+ ),
421
+ )
422
+ # Generate combinations for each location
423
+ for location, parameter_set in [
424
+ ("query", operation.query),
425
+ ("header", operation.headers),
426
+ ("cookie", operation.cookies),
427
+ ]:
428
+ if not parameter_set:
429
+ continue
430
+
431
+ container_name = LOCATION_TO_CONTAINER[location]
432
+ base_container = template.get(container_name, {})
433
+
434
+ # Get required and optional parameters
435
+ required = {p.name for p in parameter_set if p.is_required}
436
+ all_params = {p.name for p in parameter_set}
437
+ optional = sorted(all_params - required)
438
+
439
+ # Helper function to create and yield a case
440
+ def make_case(
441
+ container_values: dict,
442
+ description: str,
443
+ _location: str,
444
+ _container_name: str,
445
+ _parameter: str | None,
446
+ _generation_mode: GenerationMode,
447
+ _instant: Instant,
448
+ ) -> Case:
449
+ if _location in ("header", "cookie", "path", "query"):
450
+ container = {
451
+ name: _stringify_value(val, _location) if not isinstance(val, str) else val
452
+ for name, val in container_values.items()
453
+ }
454
+ else:
455
+ container = container_values
456
+
457
+ return operation.Case(
458
+ **{**template, _container_name: container},
459
+ meta=CaseMetadata(
460
+ generation=GenerationInfo(
461
+ time=_instant.elapsed,
462
+ mode=_generation_mode,
463
+ ),
464
+ components={},
465
+ phase=PhaseInfo.coverage(
466
+ description=description,
467
+ parameter=_parameter,
468
+ parameter_location=_location,
469
+ ),
470
+ ),
471
+ )
472
+
473
+ def _combination_schema(
474
+ combination: dict[str, Any], _required: set[str], _parameter_set: ParameterSet
475
+ ) -> dict[str, Any]:
476
+ return {
477
+ "properties": {
478
+ parameter.name: parameter.as_json_schema(operation)
479
+ for parameter in _parameter_set
480
+ if parameter.name in combination
481
+ },
482
+ "required": list(_required),
483
+ "additionalProperties": False,
484
+ }
485
+
486
+ def _yield_negative(
487
+ subschema: dict[str, Any], _location: str, _container_name: str
488
+ ) -> Generator[Case, None, None]:
489
+ iterator = iter(
490
+ coverage.cover_schema_iter(
491
+ coverage.CoverageContext(location=_location, generation_modes=[GenerationMode.NEGATIVE]),
492
+ subschema,
493
+ )
494
+ )
495
+ while True:
496
+ instant = Instant()
497
+ try:
498
+ more = next(iterator)
499
+ yield make_case(
500
+ more.value,
501
+ more.description,
502
+ _location,
503
+ _container_name,
504
+ more.parameter,
505
+ GenerationMode.NEGATIVE,
506
+ instant,
507
+ )
508
+ except StopIteration:
509
+ break
510
+
511
+ # 1. Generate only required properties
512
+ if required and all_params != required:
513
+ only_required = {k: v for k, v in base_container.items() if k in required}
514
+ if GenerationMode.POSITIVE in generation_modes:
515
+ yield make_case(
516
+ only_required,
517
+ "Only required properties",
518
+ location,
519
+ container_name,
520
+ None,
521
+ GenerationMode.POSITIVE,
522
+ Instant(),
523
+ )
524
+ if GenerationMode.NEGATIVE in generation_modes:
525
+ subschema = _combination_schema(only_required, required, parameter_set)
526
+ for case in _yield_negative(subschema, location, container_name):
527
+ assert case.meta is not None
528
+ assert isinstance(case.meta.phase.data, CoveragePhaseData)
529
+ # Already generated in one of the blocks above
530
+ if location != "path" and not case.meta.phase.data.description.startswith(
531
+ "Missing required property"
532
+ ):
533
+ yield case
534
+
535
+ # 2. Generate combinations with required properties and one optional property
536
+ for opt_param in optional:
537
+ combo = {k: v for k, v in base_container.items() if k in required or k == opt_param}
538
+ if combo != base_container and GenerationMode.POSITIVE in generation_modes:
539
+ yield make_case(
540
+ combo,
541
+ f"All required properties and optional '{opt_param}'",
542
+ location,
543
+ container_name,
544
+ None,
545
+ GenerationMode.POSITIVE,
546
+ Instant(),
547
+ )
548
+ if GenerationMode.NEGATIVE in generation_modes:
549
+ subschema = _combination_schema(combo, required, parameter_set)
550
+ for case in _yield_negative(subschema, location, container_name):
551
+ assert case.meta is not None
552
+ assert isinstance(case.meta.phase.data, CoveragePhaseData)
553
+ # Already generated in one of the blocks above
554
+ if location != "path" and not case.meta.phase.data.description.startswith(
555
+ "Missing required property"
556
+ ):
557
+ yield case
558
+
559
+ # 3. Generate one combination for each size from 2 to N-1 of optional parameters
560
+ if len(optional) > 1 and GenerationMode.POSITIVE in generation_modes:
561
+ for size in range(2, len(optional)):
562
+ for combination in combinations(optional, size):
563
+ combo = {k: v for k, v in base_container.items() if k in required or k in combination}
564
+ if combo != base_container:
565
+ yield make_case(
566
+ combo,
567
+ f"All required and {size} optional properties",
568
+ location,
569
+ container_name,
570
+ None,
571
+ GenerationMode.POSITIVE,
572
+ Instant(),
573
+ )
574
+
575
+
576
+ def find_invalid_headers(headers: Mapping) -> Generator[tuple[str, str], None, None]:
577
+ for name, value in headers.items():
578
+ if not is_latin_1_encodable(value) or has_invalid_characters(name, value):
579
+ yield name, value
580
+
581
+
582
+ UnsatisfiableExampleMark = Mark[Unsatisfiable](attr_name="unsatisfiable_example")
583
+ NonSerializableMark = Mark[SerializationNotPossible](attr_name="non_serializable")
584
+ InvalidRegexMark = Mark[SchemaError](attr_name="invalid_regex")
585
+ InvalidHeadersExampleMark = Mark[dict[str, str]](attr_name="invalid_example_header")
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from functools import lru_cache
5
+ from typing import TYPE_CHECKING, TypeVar
6
+
7
+ if TYPE_CHECKING:
8
+ from hypothesis import settings
9
+ from hypothesis import strategies as st
10
+
11
+ SCHEMATHESIS_BENCHMARK_SEED = os.environ.get("SCHEMATHESIS_BENCHMARK_SEED")
12
+
13
+
14
+ @lru_cache
15
+ def default_settings() -> settings:
16
+ from hypothesis import HealthCheck, Phase, Verbosity, settings
17
+
18
+ return settings(
19
+ database=None,
20
+ max_examples=1,
21
+ deadline=None,
22
+ verbosity=Verbosity.quiet,
23
+ phases=(Phase.generate,),
24
+ suppress_health_check=list(HealthCheck),
25
+ )
26
+
27
+
28
+ T = TypeVar("T")
29
+
30
+
31
+ def generate_one(strategy: st.SearchStrategy[T]) -> T: # type: ignore[type-var]
32
+ examples: list[T] = []
33
+ add_single_example(strategy, examples)
34
+ return examples[0]
35
+
36
+
37
+ def add_single_example(strategy: st.SearchStrategy[T], examples: list[T]) -> None:
38
+ from hypothesis import given, seed
39
+
40
+ @given(strategy) # type: ignore
41
+ @default_settings() # type: ignore
42
+ def example_generating_inner_function(ex: T) -> None:
43
+ examples.append(ex)
44
+
45
+ example_generating_inner_function._hypothesis_internal_database_key = b"" # type: ignore
46
+
47
+ if SCHEMATHESIS_BENCHMARK_SEED is not None:
48
+ example_generating_inner_function = seed(SCHEMATHESIS_BENCHMARK_SEED)(example_generating_inner_function)
49
+
50
+ example_generating_inner_function()