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
@@ -1,755 +0,0 @@
1
- # pylint: disable=too-many-statements,too-many-branches
2
- import logging
3
- import threading
4
- import time
5
- import uuid
6
- from contextlib import contextmanager
7
- from types import TracebackType
8
- from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Type, Union, cast
9
- from warnings import WarningMessage, catch_warnings
10
-
11
- import attr
12
- import hypothesis
13
- import requests
14
- from _pytest.logging import LogCaptureHandler, catching_logs
15
- from hypothesis.errors import HypothesisException, InvalidArgument
16
- from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
17
- from requests.auth import HTTPDigestAuth, _basic_auth_str
18
-
19
- from ... import failures, hooks
20
- from ...constants import (
21
- DEFAULT_STATEFUL_RECURSION_LIMIT,
22
- RECURSIVE_REFERENCE_ERROR_MESSAGE,
23
- USER_AGENT,
24
- DataGenerationMethod,
25
- )
26
- from ...exceptions import (
27
- CheckFailed,
28
- DeadlineExceeded,
29
- InvalidRegularExpression,
30
- InvalidSchema,
31
- NonCheckError,
32
- get_grouped_exception,
33
- )
34
- from ...hooks import HookContext, get_all_by_name
35
- from ...models import APIOperation, Case, Check, CheckFunction, Status, TestResult, TestResultSet
36
- from ...runner import events
37
- from ...schemas import BaseSchema
38
- from ...stateful import Feedback, Stateful
39
- from ...targets import Target, TargetContext
40
- from ...types import RawAuth, RequestCert
41
- from ...utils import (
42
- GenericResponse,
43
- Ok,
44
- WSGIResponse,
45
- capture_hypothesis_output,
46
- format_exception,
47
- maybe_set_assertion_message,
48
- )
49
- from ..serialization import SerializedTestResult
50
-
51
-
52
- @attr.s # pragma: no mutate
53
- class BaseRunner:
54
- schema: BaseSchema = attr.ib() # pragma: no mutate
55
- checks: Iterable[CheckFunction] = attr.ib() # pragma: no mutate
56
- max_response_time: Optional[int] = attr.ib() # pragma: no mutate
57
- targets: Iterable[Target] = attr.ib() # pragma: no mutate
58
- hypothesis_settings: hypothesis.settings = attr.ib() # pragma: no mutate
59
- auth: Optional[RawAuth] = attr.ib(default=None) # pragma: no mutate
60
- auth_type: Optional[str] = attr.ib(default=None) # pragma: no mutate
61
- headers: Optional[Dict[str, Any]] = attr.ib(default=None) # pragma: no mutate
62
- request_timeout: Optional[int] = attr.ib(default=None) # pragma: no mutate
63
- store_interactions: bool = attr.ib(default=False) # pragma: no mutate
64
- seed: Optional[int] = attr.ib(default=None) # pragma: no mutate
65
- exit_first: bool = attr.ib(default=False) # pragma: no mutate
66
- dry_run: bool = attr.ib(default=False) # pragma: no mutate
67
- stateful: Optional[Stateful] = attr.ib(default=None) # pragma: no mutate
68
- stateful_recursion_limit: int = attr.ib(default=DEFAULT_STATEFUL_RECURSION_LIMIT) # pragma: no mutate
69
- count_operations: bool = attr.ib(default=True) # pragma: no mutate
70
-
71
- def execute(self) -> "EventStream":
72
- """Common logic for all runners."""
73
- event = threading.Event()
74
- return EventStream(self._generate_events(event), event)
75
-
76
- def _generate_events(self, stop_event: threading.Event) -> Generator[events.ExecutionEvent, None, None]:
77
- results = TestResultSet()
78
-
79
- initialized = events.Initialized.from_schema(schema=self.schema, count_operations=self.count_operations)
80
-
81
- def _finish() -> events.Finished:
82
- return events.Finished.from_results(results=results, running_time=time.monotonic() - initialized.start_time)
83
-
84
- if stop_event.is_set():
85
- yield _finish()
86
- return
87
-
88
- yield initialized
89
-
90
- if stop_event.is_set():
91
- yield _finish()
92
- return
93
-
94
- try:
95
- for event in self._execute(results, stop_event):
96
- yield event
97
- except KeyboardInterrupt:
98
- yield events.Interrupted()
99
-
100
- yield _finish()
101
-
102
- def _should_stop(self, event: events.ExecutionEvent) -> bool:
103
- return (
104
- self.exit_first
105
- and isinstance(event, events.AfterExecution)
106
- and event.status in (Status.error, Status.failure)
107
- )
108
-
109
- def _execute(
110
- self, results: TestResultSet, stop_event: threading.Event
111
- ) -> Generator[events.ExecutionEvent, None, None]:
112
- raise NotImplementedError
113
-
114
- def _run_tests(
115
- self,
116
- maker: Callable,
117
- template: Callable,
118
- settings: hypothesis.settings,
119
- seed: Optional[int],
120
- results: TestResultSet,
121
- recursion_level: int = 0,
122
- **kwargs: Any,
123
- ) -> Generator[events.ExecutionEvent, None, None]:
124
- """Run tests and recursively run additional tests."""
125
- if recursion_level > self.stateful_recursion_limit:
126
- return
127
- for result, data_generation_method in maker(template, settings, seed):
128
- if isinstance(result, Ok):
129
- operation, test = result.ok()
130
- feedback = Feedback(self.stateful, operation)
131
- for event in run_test(
132
- operation,
133
- test,
134
- results=results,
135
- feedback=feedback,
136
- recursion_level=recursion_level,
137
- data_generation_method=data_generation_method,
138
- **kwargs,
139
- ):
140
- yield event
141
- if isinstance(event, events.Interrupted):
142
- return
143
- # Additional tests, generated via the `feedback` instance
144
- yield from self._run_tests(
145
- feedback.get_stateful_tests,
146
- template,
147
- settings,
148
- seed,
149
- recursion_level=recursion_level + 1,
150
- results=results,
151
- **kwargs,
152
- )
153
- else:
154
- # Schema errors
155
- yield from handle_schema_error(result.err(), results, data_generation_method, recursion_level)
156
-
157
-
158
- @attr.s(slots=True) # pragma: no mutate
159
- class EventStream:
160
- """Schemathesis event stream.
161
-
162
- Provides an API to control the execution flow.
163
- """
164
-
165
- generator: Generator[events.ExecutionEvent, None, None] = attr.ib() # pragma: no mutate
166
- stop_event: threading.Event = attr.ib() # pragma: no mutate
167
-
168
- def __next__(self) -> events.ExecutionEvent:
169
- return next(self.generator)
170
-
171
- def __iter__(self) -> Generator[events.ExecutionEvent, None, None]:
172
- return self.generator
173
-
174
- def stop(self) -> None:
175
- """Stop the event stream.
176
-
177
- Its next value will be the last one (Finished).
178
- """
179
- self.stop_event.set()
180
-
181
- def finish(self) -> events.ExecutionEvent:
182
- """Stop the event stream & return the last event."""
183
- self.stop()
184
- return next(self)
185
-
186
-
187
- def handle_schema_error(
188
- error: InvalidSchema, results: TestResultSet, data_generation_method: DataGenerationMethod, recursion_level: int
189
- ) -> Generator[events.ExecutionEvent, None, None]:
190
- if error.method is not None:
191
- assert error.path is not None
192
- assert error.full_path is not None
193
- method = error.method.upper()
194
- verbose_name = f"{method} {error.path}"
195
- result = TestResult(
196
- method=method,
197
- path=error.full_path,
198
- verbose_name=verbose_name,
199
- data_generation_method=data_generation_method,
200
- )
201
- result.add_error(error)
202
- correlation_id = uuid.uuid4().hex
203
- yield events.BeforeExecution(
204
- method=method,
205
- path=error.full_path,
206
- verbose_name=verbose_name,
207
- relative_path=error.path,
208
- recursion_level=recursion_level,
209
- data_generation_method=data_generation_method,
210
- correlation_id=correlation_id,
211
- )
212
- yield events.AfterExecution(
213
- method=method,
214
- path=error.full_path,
215
- relative_path=error.path,
216
- verbose_name=verbose_name,
217
- status=Status.error,
218
- result=SerializedTestResult.from_test_result(result),
219
- data_generation_method=data_generation_method,
220
- elapsed_time=0.0,
221
- hypothesis_output=[],
222
- correlation_id=correlation_id,
223
- )
224
- results.append(result)
225
- else:
226
- # When there is no `method`, then the schema error may cover multiple operations and we can't display it in
227
- # the progress bar
228
- results.generic_errors.append(error)
229
-
230
-
231
- def run_test( # pylint: disable=too-many-locals
232
- operation: APIOperation,
233
- test: Callable,
234
- checks: Iterable[CheckFunction],
235
- data_generation_method: DataGenerationMethod,
236
- targets: Iterable[Target],
237
- results: TestResultSet,
238
- headers: Optional[Dict[str, Any]],
239
- recursion_level: int,
240
- **kwargs: Any,
241
- ) -> Generator[events.ExecutionEvent, None, None]:
242
- """A single test run with all error handling needed."""
243
- result = TestResult(
244
- method=operation.method.upper(),
245
- path=operation.full_path,
246
- verbose_name=operation.verbose_name,
247
- overridden_headers=headers,
248
- data_generation_method=data_generation_method,
249
- )
250
- # To simplify connecting `before` and `after` events in external systems
251
- correlation_id = uuid.uuid4().hex
252
- yield events.BeforeExecution.from_operation(
253
- operation=operation,
254
- recursion_level=recursion_level,
255
- data_generation_method=data_generation_method,
256
- correlation_id=correlation_id,
257
- )
258
- hypothesis_output: List[str] = []
259
- errors: List[Exception] = []
260
- test_start_time = time.monotonic()
261
- setup_hypothesis_database_key(test, operation)
262
- try:
263
- with catch_warnings(record=True) as warnings, capture_hypothesis_output() as hypothesis_output:
264
- test(checks, targets, result, errors=errors, headers=headers, **kwargs)
265
- status = Status.success
266
- except CheckFailed:
267
- status = Status.failure
268
- except NonCheckError:
269
- # It could be an error in user-defined extensions, network errors or internal Schemathesis errors
270
- status = Status.error
271
- result.mark_errored()
272
- for error in deduplicate_errors(errors):
273
- result.add_error(error)
274
- except hypothesis.errors.MultipleFailures:
275
- # Schemathesis may detect multiple errors that come from different check results
276
- # They raise different "grouped" exceptions, and `MultipleFailures` is risen as the result
277
- status = Status.failure
278
- except hypothesis.errors.Flaky:
279
- status = Status.error
280
- result.mark_errored()
281
- result.add_error(
282
- hypothesis.errors.Flaky(
283
- "Tests on this API operation produce unreliable results: \n"
284
- "Falsified on the first call but did not on a subsequent one"
285
- ),
286
- result.checks[-1].example if result.checks else None,
287
- )
288
- except hypothesis.errors.Unsatisfiable:
289
- # We need more clear error message here
290
- status = Status.error
291
- result.add_error(hypothesis.errors.Unsatisfiable("Unable to satisfy schema parameters for this API operation"))
292
- except KeyboardInterrupt:
293
- yield events.Interrupted()
294
- return
295
- except AssertionError as exc: # comes from `hypothesis-jsonschema`
296
- error = reraise(exc)
297
- status = Status.error
298
- result.add_error(error)
299
- except HypothesisRefResolutionError:
300
- status = Status.error
301
- result.add_error(hypothesis.errors.Unsatisfiable(RECURSIVE_REFERENCE_ERROR_MESSAGE))
302
- except InvalidArgument as error:
303
- status = Status.error
304
- message = get_invalid_regular_expression_message(warnings)
305
- if message:
306
- # `hypothesis-jsonschema` emits a warning on invalid regular expression syntax
307
- result.add_error(InvalidRegularExpression(message))
308
- else:
309
- result.add_error(error)
310
- except hypothesis.errors.DeadlineExceeded as error:
311
- status = Status.error
312
- result.add_error(DeadlineExceeded.from_exc(error))
313
- except Exception as error:
314
- status = Status.error
315
- result.add_error(error)
316
- test_elapsed_time = time.monotonic() - test_start_time
317
- # Fetch seed value, hypothesis generates it during test execution
318
- # It may be `None` if the `derandomize` config option is set to `True`
319
- result.seed = getattr(test, "_hypothesis_internal_use_seed", None) or getattr(
320
- test, "_hypothesis_internal_use_generated_seed", None
321
- )
322
- results.append(result)
323
- yield events.AfterExecution.from_result(
324
- result=result,
325
- status=status,
326
- elapsed_time=test_elapsed_time,
327
- hypothesis_output=hypothesis_output,
328
- operation=operation,
329
- data_generation_method=data_generation_method,
330
- correlation_id=correlation_id,
331
- )
332
-
333
-
334
- def setup_hypothesis_database_key(test: Callable, operation: APIOperation) -> None:
335
- """Make Hypothesis use separate database entries for every API operation.
336
-
337
- It increases the effectiveness of the Hypothesis database in the CLI.
338
- """
339
- # Hypothesis's function digest depends on the test function signature. To reflect it for the web API case,
340
- # we use all API operation parameters in the digest.
341
- extra = operation.verbose_name.encode("utf8")
342
- for parameter in operation.definition.parameters:
343
- extra += parameter.serialize().encode("utf8")
344
- test.hypothesis.inner_test._hypothesis_internal_add_digest = extra # type: ignore
345
-
346
-
347
- def get_invalid_regular_expression_message(warnings: List[WarningMessage]) -> Optional[str]:
348
- for warning in warnings:
349
- message = str(warning.message)
350
- if "is not valid syntax for a Python regular expression" in message:
351
- return message
352
- return None
353
-
354
-
355
- def reraise(error: AssertionError) -> InvalidSchema:
356
- traceback = format_exception(error, True)
357
- if "assert type_ in TYPE_STRINGS" in traceback:
358
- message = "Invalid type name"
359
- else:
360
- message = "Unknown schema error"
361
- try:
362
- raise InvalidSchema(message) from error
363
- except InvalidSchema as exc:
364
- return exc
365
-
366
-
367
- def deduplicate_errors(errors: List[Exception]) -> Generator[Exception, None, None]:
368
- """Deduplicate errors by their messages + tracebacks."""
369
- seen = set()
370
- for error in errors:
371
- message = format_exception(error, True)
372
- if message in seen:
373
- continue
374
- seen.add(message)
375
- yield error
376
-
377
-
378
- def run_checks(
379
- case: Case,
380
- checks: Iterable[CheckFunction],
381
- check_results: List[Check],
382
- result: TestResult,
383
- response: GenericResponse,
384
- elapsed_time: float,
385
- max_response_time: Optional[int] = None,
386
- ) -> None:
387
- errors = []
388
-
389
- for check in checks:
390
- check_name = check.__name__
391
- try:
392
- skip_check = check(response, case)
393
- if not skip_check:
394
- check_result = result.add_success(check_name, case, response, elapsed_time)
395
- check_results.append(check_result)
396
- except AssertionError as exc:
397
- message = maybe_set_assertion_message(exc, check_name)
398
- errors.append(exc)
399
- if isinstance(exc, CheckFailed):
400
- context = exc.context
401
- else:
402
- context = None
403
- check_result = result.add_failure(check_name, case, response, elapsed_time, message, context)
404
- check_results.append(check_result)
405
-
406
- if max_response_time:
407
- if elapsed_time > max_response_time:
408
- message = f"Response time exceeded the limit of {max_response_time} ms"
409
- errors.append(AssertionError(message))
410
- result.add_failure(
411
- "max_response_time",
412
- case,
413
- response,
414
- elapsed_time,
415
- message,
416
- failures.ResponseTimeExceeded(elapsed=elapsed_time, deadline=max_response_time),
417
- )
418
- else:
419
- result.add_success("max_response_time", case, response, elapsed_time)
420
-
421
- if errors:
422
- raise get_grouped_exception(case.operation.verbose_name, *errors)
423
-
424
-
425
- def run_targets(targets: Iterable[Callable], context: TargetContext) -> None:
426
- for target in targets:
427
- value = target(context)
428
- hypothesis.target(value, label=target.__name__)
429
-
430
-
431
- def add_cases(case: Case, response: GenericResponse, test: Callable, *args: Any) -> None:
432
- context = HookContext(case.operation)
433
- for case_hook in get_all_by_name("add_case"):
434
- _case = case_hook(context, case.partial_deepcopy(), response)
435
- # run additional test if _case is not an empty value
436
- if _case:
437
- test(_case, *args)
438
-
439
-
440
- @attr.s(slots=True) # pragma: no mutate
441
- class ErrorCollector:
442
- """Collect exceptions that are not related to failed checks.
443
-
444
- Such exceptions may be considered as multiple failures or flakiness by Hypothesis. In both cases, Hypothesis hides
445
- exception information that, in our case, is helpful for the end-user. It either indicates errors in user-defined
446
- extensions, network-related errors, or internal Schemathesis errors. In all cases, this information is useful for
447
- debugging.
448
-
449
- To mitigate this, we gather all exceptions manually via this context manager to avoid interfering with the test
450
- function signatures, which are used by Hypothesis.
451
- """
452
-
453
- errors: List[Exception] = attr.ib() # pragma: no mutate
454
-
455
- def __enter__(self) -> "ErrorCollector":
456
- return self
457
-
458
- # Typing: The return type suggested by mypy is `Literal[False]`, but I don't want to introduce dependency on the
459
- # `typing_extensions` package for Python 3.7
460
- def __exit__(
461
- self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType]
462
- ) -> Any:
463
- # Don't do anything special if:
464
- # - Tests are successful
465
- # - Checks failed
466
- # - The testing process is interrupted
467
- if not exc_type or issubclass(exc_type, CheckFailed) or not issubclass(exc_type, Exception):
468
- return False
469
- # These exceptions are needed for control flow on the Hypothesis side. E.g. rejecting unsatisfiable examples
470
- if isinstance(exc_val, HypothesisException):
471
- raise
472
- # Exception value is not `None` and is a subclass of `Exception` at this point
473
- exc_val = cast(Exception, exc_val)
474
- self.errors.append(exc_val.with_traceback(exc_tb))
475
- raise NonCheckError from None
476
-
477
-
478
- def network_test(
479
- case: Case,
480
- checks: Iterable[CheckFunction],
481
- targets: Iterable[Target],
482
- result: TestResult,
483
- session: requests.Session,
484
- request_timeout: Optional[int],
485
- request_tls_verify: bool,
486
- request_cert: Optional[RequestCert],
487
- store_interactions: bool,
488
- headers: Optional[Dict[str, Any]],
489
- feedback: Feedback,
490
- max_response_time: Optional[int],
491
- dry_run: bool,
492
- errors: List[Exception],
493
- ) -> None:
494
- """A single test body will be executed against the target."""
495
- with ErrorCollector(errors):
496
- headers = headers or {}
497
- if "user-agent" not in {header.lower() for header in headers}:
498
- headers["User-Agent"] = USER_AGENT
499
- timeout = prepare_timeout(request_timeout)
500
- if not dry_run:
501
- response = _network_test(
502
- case,
503
- checks,
504
- targets,
505
- result,
506
- session,
507
- timeout,
508
- store_interactions,
509
- headers,
510
- feedback,
511
- request_tls_verify,
512
- request_cert,
513
- max_response_time,
514
- )
515
- add_cases(
516
- case,
517
- response,
518
- _network_test,
519
- checks,
520
- targets,
521
- result,
522
- session,
523
- timeout,
524
- store_interactions,
525
- headers,
526
- feedback,
527
- request_tls_verify,
528
- request_cert,
529
- max_response_time,
530
- )
531
-
532
-
533
- def _network_test(
534
- case: Case,
535
- checks: Iterable[CheckFunction],
536
- targets: Iterable[Target],
537
- result: TestResult,
538
- session: requests.Session,
539
- timeout: Optional[float],
540
- store_interactions: bool,
541
- headers: Optional[Dict[str, Any]],
542
- feedback: Feedback,
543
- request_tls_verify: bool,
544
- request_cert: Optional[RequestCert],
545
- max_response_time: Optional[int],
546
- ) -> requests.Response:
547
- check_results: List[Check] = []
548
- try:
549
- hook_context = HookContext(operation=case.operation)
550
- hooks.dispatch("before_call", hook_context, case)
551
- kwargs: Dict[str, Any] = {
552
- "session": session,
553
- "headers": headers,
554
- "timeout": timeout,
555
- "verify": request_tls_verify,
556
- "cert": request_cert,
557
- }
558
- hooks.dispatch("process_call_kwargs", hook_context, case, kwargs)
559
- response = case.call(**kwargs)
560
- hooks.dispatch("after_call", hook_context, case, response)
561
- except CheckFailed as exc:
562
- check_name = "request_timeout"
563
- requests_kwargs = case.as_requests_kwargs(base_url=case.get_full_base_url(), headers=headers)
564
- request = requests.Request(**requests_kwargs).prepare()
565
- elapsed = cast(float, timeout) # It is defined and not empty, since the exception happened
566
- check_result = result.add_failure(
567
- check_name, case, None, elapsed, f"Response timed out after {1000 * elapsed:.2f}ms", exc.context, request
568
- )
569
- check_results.append(check_result)
570
- raise exc
571
- context = TargetContext(case=case, response=response, response_time=response.elapsed.total_seconds())
572
- run_targets(targets, context)
573
- status = Status.success
574
- try:
575
- run_checks(case, checks, check_results, result, response, context.response_time * 1000, max_response_time)
576
- except CheckFailed:
577
- status = Status.failure
578
- raise
579
- finally:
580
- if store_interactions:
581
- result.store_requests_response(response, status, check_results)
582
- feedback.add_test_case(case, response)
583
- return response
584
-
585
-
586
- @contextmanager
587
- def get_session(auth: Optional[Union[HTTPDigestAuth, RawAuth]] = None) -> Generator[requests.Session, None, None]:
588
- with requests.Session() as session:
589
- if auth is not None:
590
- session.auth = auth
591
- yield session
592
-
593
-
594
- def prepare_timeout(timeout: Optional[int]) -> Optional[float]:
595
- """Request timeout is in milliseconds, but `requests` uses seconds."""
596
- output: Optional[Union[int, float]] = timeout
597
- if timeout is not None:
598
- output = timeout / 1000
599
- return output
600
-
601
-
602
- def wsgi_test(
603
- case: Case,
604
- checks: Iterable[CheckFunction],
605
- targets: Iterable[Target],
606
- result: TestResult,
607
- auth: Optional[RawAuth],
608
- auth_type: Optional[str],
609
- headers: Optional[Dict[str, Any]],
610
- store_interactions: bool,
611
- feedback: Feedback,
612
- max_response_time: Optional[int],
613
- dry_run: bool,
614
- errors: List[Exception],
615
- ) -> None:
616
- with ErrorCollector(errors):
617
- headers = _prepare_wsgi_headers(headers, auth, auth_type)
618
- if not dry_run:
619
- response = _wsgi_test(
620
- case, checks, targets, result, headers, store_interactions, feedback, max_response_time
621
- )
622
- add_cases(
623
- case,
624
- response,
625
- _wsgi_test,
626
- checks,
627
- targets,
628
- result,
629
- headers,
630
- store_interactions,
631
- feedback,
632
- max_response_time,
633
- )
634
-
635
-
636
- def _wsgi_test(
637
- case: Case,
638
- checks: Iterable[CheckFunction],
639
- targets: Iterable[Target],
640
- result: TestResult,
641
- headers: Dict[str, Any],
642
- store_interactions: bool,
643
- feedback: Feedback,
644
- max_response_time: Optional[int],
645
- ) -> WSGIResponse:
646
- with catching_logs(LogCaptureHandler(), level=logging.DEBUG) as recorded:
647
- start = time.monotonic()
648
- hook_context = HookContext(operation=case.operation)
649
- hooks.dispatch("before_call", hook_context, case)
650
- kwargs = {"headers": headers}
651
- hooks.dispatch("process_call_kwargs", hook_context, case, kwargs)
652
- response = case.call_wsgi(**kwargs)
653
- hooks.dispatch("after_call", hook_context, case, response)
654
- elapsed = time.monotonic() - start
655
- context = TargetContext(case=case, response=response, response_time=elapsed)
656
- run_targets(targets, context)
657
- result.logs.extend(recorded.records)
658
- status = Status.success
659
- check_results: List[Check] = []
660
- try:
661
- run_checks(case, checks, check_results, result, response, context.response_time * 1000, max_response_time)
662
- except CheckFailed:
663
- status = Status.failure
664
- raise
665
- finally:
666
- if store_interactions:
667
- result.store_wsgi_response(case, response, headers, elapsed, status, check_results)
668
- feedback.add_test_case(case, response)
669
- return response
670
-
671
-
672
- def _prepare_wsgi_headers(
673
- headers: Optional[Dict[str, Any]], auth: Optional[RawAuth], auth_type: Optional[str]
674
- ) -> Dict[str, Any]:
675
- headers = headers or {}
676
- if "user-agent" not in {header.lower() for header in headers}:
677
- headers["User-Agent"] = USER_AGENT
678
- wsgi_auth = get_wsgi_auth(auth, auth_type)
679
- if wsgi_auth:
680
- headers["Authorization"] = wsgi_auth
681
- return headers
682
-
683
-
684
- def get_wsgi_auth(auth: Optional[RawAuth], auth_type: Optional[str]) -> Optional[str]:
685
- if auth:
686
- if auth_type == "digest":
687
- raise ValueError("Digest auth is not supported for WSGI apps")
688
- return _basic_auth_str(*auth)
689
- return None
690
-
691
-
692
- def asgi_test(
693
- case: Case,
694
- checks: Iterable[CheckFunction],
695
- targets: Iterable[Target],
696
- result: TestResult,
697
- store_interactions: bool,
698
- headers: Optional[Dict[str, Any]],
699
- feedback: Feedback,
700
- max_response_time: Optional[int],
701
- dry_run: bool,
702
- errors: List[Exception],
703
- ) -> None:
704
- """A single test body will be executed against the target."""
705
- with ErrorCollector(errors):
706
- headers = headers or {}
707
-
708
- if not dry_run:
709
- response = _asgi_test(
710
- case, checks, targets, result, store_interactions, headers, feedback, max_response_time
711
- )
712
- add_cases(
713
- case,
714
- response,
715
- _asgi_test,
716
- checks,
717
- targets,
718
- result,
719
- store_interactions,
720
- headers,
721
- feedback,
722
- max_response_time,
723
- )
724
-
725
-
726
- def _asgi_test(
727
- case: Case,
728
- checks: Iterable[CheckFunction],
729
- targets: Iterable[Target],
730
- result: TestResult,
731
- store_interactions: bool,
732
- headers: Optional[Dict[str, Any]],
733
- feedback: Feedback,
734
- max_response_time: Optional[int],
735
- ) -> requests.Response:
736
- hook_context = HookContext(operation=case.operation)
737
- hooks.dispatch("before_call", hook_context, case)
738
- kwargs: Dict[str, Any] = {"headers": headers}
739
- hooks.dispatch("process_call_kwargs", hook_context, case, kwargs)
740
- response = case.call_asgi(**kwargs)
741
- hooks.dispatch("after_call", hook_context, case, response)
742
- context = TargetContext(case=case, response=response, response_time=response.elapsed.total_seconds())
743
- run_targets(targets, context)
744
- status = Status.success
745
- check_results: List[Check] = []
746
- try:
747
- run_checks(case, checks, check_results, result, response, context.response_time * 1000, max_response_time)
748
- except CheckFailed:
749
- status = Status.failure
750
- raise
751
- finally:
752
- if store_interactions:
753
- result.store_requests_response(response, status, check_results)
754
- feedback.add_test_case(case, response)
755
- return response