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
@@ -1,1280 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import functools
4
- import logging
5
- import operator
6
- import re
7
- import threading
8
- import time
9
- import unittest
10
- import uuid
11
- import warnings
12
- from contextlib import contextmanager
13
- from dataclasses import dataclass, field
14
- from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, List, Literal, cast
15
- from warnings import WarningMessage, catch_warnings
16
-
17
- import hypothesis
18
- import requests
19
- from _pytest.logging import LogCaptureHandler, catching_logs
20
- from hypothesis.errors import HypothesisException, InvalidArgument
21
- from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
22
- from jsonschema.exceptions import SchemaError as JsonSchemaError
23
- from jsonschema.exceptions import ValidationError
24
- from requests.structures import CaseInsensitiveDict
25
- from urllib3.exceptions import InsecureRequestWarning
26
-
27
- from ... import experimental, failures, hooks
28
- from ..._compat import MultipleFailures
29
- from ..._hypothesis import (
30
- get_invalid_example_headers_mark,
31
- get_invalid_regex_mark,
32
- get_non_serializable_mark,
33
- has_unsatisfied_example_mark,
34
- )
35
- from ...auths import unregister as unregister_auth
36
- from ...checks import _make_max_response_time_failure_message
37
- from ...constants import (
38
- DEFAULT_STATEFUL_RECURSION_LIMIT,
39
- RECURSIVE_REFERENCE_ERROR_MESSAGE,
40
- SERIALIZERS_SUGGESTION_MESSAGE,
41
- USER_AGENT,
42
- )
43
- from ...exceptions import (
44
- CheckFailed,
45
- DeadlineExceeded,
46
- InternalError,
47
- InvalidHeadersExample,
48
- InvalidRegularExpression,
49
- NonCheckError,
50
- OperationSchemaError,
51
- RecursiveReferenceError,
52
- SerializationNotPossible,
53
- SkipTest,
54
- format_exception,
55
- get_grouped_exception,
56
- maybe_set_assertion_message,
57
- )
58
- from ...generation import DataGenerationMethod, GenerationConfig
59
- from ...hooks import HookContext, get_all_by_name
60
- from ...internal.checks import CheckConfig, CheckContext
61
- from ...internal.datetime import current_datetime
62
- from ...internal.result import Err, Ok, Result
63
- from ...models import APIOperation, Case, Check, Status, TestResult
64
- from ...runner import events
65
- from ...service import extensions
66
- from ...service.models import AnalysisResult, AnalysisSuccess
67
- from ...specs.openapi import formats
68
- from ...stateful import Feedback, Stateful
69
- from ...stateful import events as stateful_events
70
- from ...stateful import runner as stateful_runner
71
- from ...targets import Target, TargetContext
72
- from ...transports import RequestConfig, RequestsTransport
73
- from ...transports.auth import get_requests_auth, prepare_wsgi_headers
74
- from ...utils import capture_hypothesis_output
75
- from .. import probes
76
- from ..serialization import SerializedTestResult
77
- from .context import RunnerContext
78
-
79
- if TYPE_CHECKING:
80
- from types import TracebackType
81
-
82
- from requests.auth import HTTPDigestAuth
83
-
84
- from ..._override import CaseOverride
85
- from ...internal.checks import CheckFunction
86
- from ...schemas import BaseSchema
87
- from ...service.client import ServiceClient
88
- from ...transports.responses import GenericResponse, WSGIResponse
89
- from ...types import RawAuth
90
-
91
-
92
- def _should_count_towards_stop(event: events.ExecutionEvent) -> bool:
93
- return isinstance(event, events.AfterExecution) and event.status in (Status.error, Status.failure)
94
-
95
-
96
- @dataclass
97
- class BaseRunner:
98
- schema: BaseSchema
99
- checks: Iterable[CheckFunction]
100
- max_response_time: int | None
101
- targets: Iterable[Target]
102
- hypothesis_settings: hypothesis.settings
103
- generation_config: GenerationConfig | None
104
- probe_config: probes.ProbeConfig
105
- checks_config: CheckConfig
106
- request_config: RequestConfig = field(default_factory=RequestConfig)
107
- override: CaseOverride | None = None
108
- auth: RawAuth | None = None
109
- auth_type: str | None = None
110
- headers: dict[str, Any] | None = None
111
- store_interactions: bool = False
112
- seed: int | None = None
113
- exit_first: bool = False
114
- no_failfast: bool = False
115
- max_failures: int | None = None
116
- started_at: str = field(default_factory=current_datetime)
117
- unique_data: bool = False
118
- dry_run: bool = False
119
- stateful: Stateful | None = None
120
- stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT
121
- count_operations: bool = True
122
- count_links: bool = True
123
- service_client: ServiceClient | None = None
124
- _failures_counter: int = 0
125
- _is_stopping_due_to_failure_limit: bool = False
126
-
127
- def execute(self) -> EventStream:
128
- """Common logic for all runners."""
129
- event = threading.Event()
130
- return EventStream(self._generate_events(event), event)
131
-
132
- def _generate_events(self, stop_event: threading.Event) -> Generator[events.ExecutionEvent, None, None]:
133
- # If auth is explicitly provided, then the global provider is ignored
134
- if self.auth is not None:
135
- unregister_auth()
136
- ctx = RunnerContext(
137
- auth=self.auth,
138
- seed=self.seed,
139
- stop_event=stop_event,
140
- unique_data=self.unique_data,
141
- checks_config=self.checks_config,
142
- override=self.override,
143
- no_failfast=self.no_failfast,
144
- )
145
- start_time = time.monotonic()
146
- initialized = None
147
- __probes = None
148
- __analysis: Result[AnalysisResult, Exception] | None = None
149
-
150
- def _should_warn_about_only_4xx(result: TestResult) -> bool:
151
- if all(check.response is None for check in result.checks):
152
- return False
153
- # Don't duplicate auth warnings
154
- if {check.response.status_code for check in result.checks if check.response is not None} <= {401, 403}:
155
- return False
156
- # At this point we know we only have 4xx responses
157
- return True
158
-
159
- def _check_warnings() -> None:
160
- # Warn if all positive test cases got 4xx in return and no failure was found
161
- def all_positive_are_rejected(result: TestResult) -> bool:
162
- seen_positive = False
163
- for check in result.checks:
164
- if check.example.data_generation_method != DataGenerationMethod.positive:
165
- continue
166
- seen_positive = True
167
- if check.response is None:
168
- continue
169
- # At least one positive response for positive test case
170
- if 200 <= check.response.status_code < 300:
171
- return False
172
- # If there are positive test cases, and we ended up here, then there are no 2xx responses for them
173
- # Otherwise, there are no positive test cases at all and this check should pass
174
- return seen_positive
175
-
176
- for result in ctx.data.results:
177
- # Only warn about 4xx responses in successful positive test scenarios
178
- if (
179
- all(check.value == Status.success for check in result.checks)
180
- and DataGenerationMethod.positive in result.data_generation_method
181
- and all_positive_are_rejected(result)
182
- and _should_warn_about_only_4xx(result)
183
- ):
184
- ctx.add_warning(
185
- f"`{result.verbose_name}` returned only 4xx responses during unit tests. Check base URL or adjust data generation settings"
186
- )
187
-
188
- def _initialize() -> events.Initialized:
189
- nonlocal initialized
190
- initialized = events.Initialized.from_schema(
191
- schema=self.schema,
192
- count_operations=self.count_operations,
193
- count_links=self.count_links,
194
- seed=ctx.seed,
195
- start_time=start_time,
196
- )
197
- return initialized
198
-
199
- def _finish() -> events.Finished:
200
- _check_warnings()
201
- return events.Finished.from_results(results=ctx.data, running_time=time.monotonic() - start_time)
202
-
203
- def _before_probes() -> events.BeforeProbing:
204
- return events.BeforeProbing()
205
-
206
- def _run_probes() -> None:
207
- if not self.dry_run:
208
- nonlocal __probes
209
-
210
- __probes = run_probes(self.schema, self.probe_config)
211
-
212
- def _after_probes() -> events.AfterProbing:
213
- _probes = cast(List[probes.ProbeRun], __probes)
214
- return events.AfterProbing(probes=_probes)
215
-
216
- def _before_analysis() -> events.BeforeAnalysis:
217
- return events.BeforeAnalysis()
218
-
219
- def _run_analysis() -> None:
220
- nonlocal __analysis, __probes
221
-
222
- if self.service_client is not None:
223
- try:
224
- _probes = cast(List[probes.ProbeRun], __probes)
225
- result = self.service_client.analyze_schema(_probes, self.schema.raw_schema)
226
- if isinstance(result, AnalysisSuccess):
227
- extensions.apply(result.extensions, self.schema)
228
- __analysis = Ok(result)
229
- except Exception as exc:
230
- __analysis = Err(exc)
231
-
232
- def _after_analysis() -> events.AfterAnalysis:
233
- return events.AfterAnalysis(analysis=__analysis)
234
-
235
- if ctx.is_stopped:
236
- yield _finish()
237
- return
238
-
239
- for event_factory in (
240
- _initialize,
241
- _before_probes,
242
- _run_probes,
243
- _after_probes,
244
- _before_analysis,
245
- _run_analysis,
246
- _after_analysis,
247
- ):
248
- event = event_factory()
249
- if event is not None:
250
- yield event
251
- if ctx.is_stopped:
252
- yield _finish() # type: ignore[unreachable]
253
- return
254
-
255
- try:
256
- warnings.simplefilter("ignore", InsecureRequestWarning)
257
- if not experimental.STATEFUL_ONLY.is_enabled:
258
- yield from self._execute(ctx)
259
- if not self._is_stopping_due_to_failure_limit:
260
- yield from self._run_stateful_tests(ctx)
261
- except KeyboardInterrupt:
262
- yield events.Interrupted()
263
-
264
- yield _finish()
265
-
266
- def _should_stop(self, event: events.ExecutionEvent) -> bool:
267
- result = self.__should_stop(event)
268
- if result:
269
- self._is_stopping_due_to_failure_limit = True
270
- return result
271
-
272
- def __should_stop(self, event: events.ExecutionEvent) -> bool:
273
- if _should_count_towards_stop(event):
274
- if self.exit_first:
275
- return True
276
- if self.max_failures is not None:
277
- self._failures_counter += 1
278
- return self._failures_counter >= self.max_failures
279
- return False
280
-
281
- def _execute(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
282
- raise NotImplementedError
283
-
284
- def _run_stateful_tests(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
285
- # Run new-style stateful tests
286
- if self.stateful is not None and experimental.STATEFUL_TEST_RUNNER.is_enabled and self.schema.links_count > 0:
287
- result = TestResult(
288
- method="",
289
- path="",
290
- verbose_name="Stateful tests",
291
- seed=ctx.seed,
292
- data_generation_method=self.schema.data_generation_methods,
293
- )
294
- headers = self.headers or {}
295
- if isinstance(self.schema.transport, RequestsTransport):
296
- auth = get_requests_auth(self.auth, self.auth_type)
297
- else:
298
- auth = None
299
- headers = prepare_wsgi_headers(headers, self.auth, self.auth_type)
300
- config = stateful_runner.StatefulTestRunnerConfig(
301
- checks=tuple(self.checks),
302
- headers=headers,
303
- hypothesis_settings=self.hypothesis_settings,
304
- exit_first=self.exit_first,
305
- max_failures=None if self.max_failures is None else self.max_failures - self._failures_counter,
306
- request=self.request_config,
307
- auth=auth,
308
- seed=ctx.seed,
309
- override=self.override,
310
- )
311
- state_machine = self.schema.as_state_machine()
312
- runner = state_machine.runner(config=config)
313
- status = Status.success
314
-
315
- def from_step_status(step_status: stateful_events.StepStatus) -> Status:
316
- return {
317
- stateful_events.StepStatus.SUCCESS: Status.success,
318
- stateful_events.StepStatus.FAILURE: Status.failure,
319
- stateful_events.StepStatus.ERROR: Status.error,
320
- stateful_events.StepStatus.INTERRUPTED: Status.error,
321
- }[step_status]
322
-
323
- if self.store_interactions:
324
- if isinstance(state_machine.schema.transport, RequestsTransport):
325
-
326
- def on_step_finished(event: stateful_events.StepFinished) -> None:
327
- if event.response is not None and event.status is not None:
328
- response = cast(requests.Response, event.response)
329
- result.store_requests_response(
330
- status=from_step_status(event.status),
331
- case=event.case,
332
- response=response,
333
- checks=event.checks,
334
- headers=headers,
335
- session=None,
336
- )
337
-
338
- else:
339
-
340
- def on_step_finished(event: stateful_events.StepFinished) -> None:
341
- from ...transports.responses import WSGIResponse
342
-
343
- if event.response is not None and event.status is not None:
344
- response = cast(WSGIResponse, event.response)
345
- result.store_wsgi_response(
346
- status=from_step_status(event.status),
347
- case=event.case,
348
- response=response,
349
- headers=headers,
350
- elapsed=response.elapsed.total_seconds(),
351
- checks=event.checks,
352
- )
353
- else:
354
-
355
- def on_step_finished(event: stateful_events.StepFinished) -> None:
356
- return None
357
-
358
- test_start_time: float | None = None
359
- test_elapsed_time: float | None = None
360
-
361
- for stateful_event in runner.execute():
362
- if isinstance(stateful_event, stateful_events.SuiteFinished):
363
- if stateful_event.failures and status != Status.error:
364
- status = Status.failure
365
- elif isinstance(stateful_event, stateful_events.RunStarted):
366
- test_start_time = stateful_event.timestamp
367
- elif isinstance(stateful_event, stateful_events.RunFinished):
368
- test_elapsed_time = stateful_event.timestamp - cast(float, test_start_time)
369
- elif isinstance(stateful_event, stateful_events.StepFinished):
370
- result.checks.extend(stateful_event.checks)
371
- on_step_finished(stateful_event)
372
- elif isinstance(stateful_event, stateful_events.Errored):
373
- status = Status.error
374
- result.add_error(stateful_event.exception)
375
- yield events.StatefulEvent(data=stateful_event)
376
- ctx.add_result(result)
377
- yield events.AfterStatefulExecution(
378
- status=status,
379
- result=SerializedTestResult.from_test_result(result),
380
- elapsed_time=cast(float, test_elapsed_time),
381
- data_generation_method=self.schema.data_generation_methods,
382
- )
383
-
384
- def _run_tests(
385
- self,
386
- maker: Callable,
387
- test_func: Callable,
388
- settings: hypothesis.settings,
389
- generation_config: GenerationConfig | None,
390
- ctx: RunnerContext,
391
- recursion_level: int = 0,
392
- headers: dict[str, Any] | None = None,
393
- **kwargs: Any,
394
- ) -> Generator[events.ExecutionEvent, None, None]:
395
- """Run tests and recursively run additional tests."""
396
- if recursion_level > self.stateful_recursion_limit:
397
- return
398
-
399
- def as_strategy_kwargs(_operation: APIOperation) -> dict[str, Any]:
400
- kw = {}
401
- if self.override is not None:
402
- for location, entry in self.override.for_operation(_operation).items():
403
- if entry:
404
- kw[location] = entry
405
- if headers:
406
- kw["headers"] = {key: value for key, value in headers.items() if key.lower() != "user-agent"}
407
- return kw
408
-
409
- for result in maker(
410
- test_func,
411
- settings=settings,
412
- generation_config=generation_config,
413
- seed=ctx.seed,
414
- as_strategy_kwargs=as_strategy_kwargs,
415
- ):
416
- if isinstance(result, Ok):
417
- operation, test = result.ok()
418
- if self.stateful is not None and not experimental.STATEFUL_TEST_RUNNER.is_enabled:
419
- feedback = Feedback(self.stateful, operation)
420
- else:
421
- feedback = None
422
- # Track whether `BeforeExecution` was already emitted.
423
- # Schema error may happen before / after `BeforeExecution`, but it should be emitted only once
424
- # and the `AfterExecution` event should have the same correlation id as previous `BeforeExecution`
425
- before_execution_correlation_id = None
426
- try:
427
- for event in run_test(
428
- operation,
429
- test,
430
- ctx=ctx,
431
- feedback=feedback,
432
- recursion_level=recursion_level,
433
- data_generation_methods=self.schema.data_generation_methods,
434
- headers=headers,
435
- **kwargs,
436
- ):
437
- yield event
438
- if isinstance(event, events.BeforeExecution):
439
- before_execution_correlation_id = event.correlation_id
440
- if isinstance(event, events.Interrupted):
441
- return
442
- # Additional tests, generated via the `feedback` instance
443
- if feedback is not None:
444
- yield from self._run_tests(
445
- feedback.get_stateful_tests,
446
- test_func,
447
- settings=settings,
448
- generation_config=generation_config,
449
- recursion_level=recursion_level + 1,
450
- ctx=ctx,
451
- headers=headers,
452
- **kwargs,
453
- )
454
- except OperationSchemaError as exc:
455
- yield from handle_schema_error(
456
- exc,
457
- ctx,
458
- self.schema.data_generation_methods,
459
- recursion_level,
460
- before_execution_correlation_id=before_execution_correlation_id,
461
- )
462
- else:
463
- # Schema errors
464
- yield from handle_schema_error(result.err(), ctx, self.schema.data_generation_methods, recursion_level)
465
-
466
-
467
- def run_probes(schema: BaseSchema, config: probes.ProbeConfig) -> list[probes.ProbeRun]:
468
- """Discover capabilities of the tested app."""
469
- results = probes.run(schema, config)
470
- for result in results:
471
- if isinstance(result.probe, probes.NullByteInHeader) and result.is_failure:
472
- from ...specs.openapi.formats import HEADER_FORMAT, header_values
473
-
474
- formats.register(HEADER_FORMAT, header_values(blacklist_characters="\n\r\x00"))
475
- return results
476
-
477
-
478
- @dataclass
479
- class EventStream:
480
- """Schemathesis event stream.
481
-
482
- Provides an API to control the execution flow.
483
- """
484
-
485
- generator: Generator[events.ExecutionEvent, None, None]
486
- stop_event: threading.Event
487
-
488
- def __next__(self) -> events.ExecutionEvent:
489
- return next(self.generator)
490
-
491
- def __iter__(self) -> Generator[events.ExecutionEvent, None, None]:
492
- return self.generator
493
-
494
- def stop(self) -> None:
495
- """Stop the event stream.
496
-
497
- Its next value will be the last one (Finished).
498
- """
499
- self.stop_event.set()
500
-
501
- def finish(self) -> events.ExecutionEvent:
502
- """Stop the event stream & return the last event."""
503
- self.stop()
504
- return next(self)
505
-
506
-
507
- def handle_schema_error(
508
- error: OperationSchemaError,
509
- ctx: RunnerContext,
510
- data_generation_methods: Iterable[DataGenerationMethod],
511
- recursion_level: int,
512
- *,
513
- before_execution_correlation_id: str | None = None,
514
- ) -> Generator[events.ExecutionEvent, None, None]:
515
- if error.method is not None:
516
- assert error.path is not None
517
- assert error.full_path is not None
518
- data_generation_methods = list(data_generation_methods)
519
- method = error.method.upper()
520
- verbose_name = f"{method} {error.full_path}"
521
- result = TestResult(
522
- method=method,
523
- path=error.full_path,
524
- verbose_name=verbose_name,
525
- data_generation_method=data_generation_methods,
526
- )
527
- result.add_error(error)
528
- # It might be already emitted - reuse its correlation id
529
- if before_execution_correlation_id is not None:
530
- correlation_id = before_execution_correlation_id
531
- else:
532
- correlation_id = uuid.uuid4().hex
533
- yield events.BeforeExecution(
534
- method=method,
535
- path=error.full_path,
536
- verbose_name=verbose_name,
537
- relative_path=error.path,
538
- recursion_level=recursion_level,
539
- data_generation_method=data_generation_methods,
540
- correlation_id=correlation_id,
541
- )
542
- yield events.AfterExecution(
543
- method=method,
544
- path=error.full_path,
545
- relative_path=error.path,
546
- verbose_name=verbose_name,
547
- status=Status.error,
548
- result=SerializedTestResult.from_test_result(result),
549
- data_generation_method=data_generation_methods,
550
- elapsed_time=0.0,
551
- hypothesis_output=[],
552
- correlation_id=correlation_id,
553
- )
554
- ctx.add_result(result)
555
- else:
556
- # When there is no `method`, then the schema error may cover multiple operations, and we can't display it in
557
- # the progress bar
558
- ctx.add_generic_error(error)
559
-
560
-
561
- def run_test(
562
- operation: APIOperation,
563
- test: Callable,
564
- checks: Iterable[CheckFunction],
565
- data_generation_methods: Iterable[DataGenerationMethod],
566
- targets: Iterable[Target],
567
- ctx: RunnerContext,
568
- headers: dict[str, Any] | None,
569
- recursion_level: int,
570
- **kwargs: Any,
571
- ) -> Generator[events.ExecutionEvent, None, None]:
572
- """A single test run with all error handling needed."""
573
- data_generation_methods = list(data_generation_methods)
574
- result = TestResult(
575
- method=operation.method.upper(),
576
- path=operation.full_path,
577
- verbose_name=operation.verbose_name,
578
- data_generation_method=data_generation_methods,
579
- )
580
- # To simplify connecting `before` and `after` events in external systems
581
- correlation_id = uuid.uuid4().hex
582
- yield events.BeforeExecution.from_operation(
583
- operation=operation,
584
- recursion_level=recursion_level,
585
- data_generation_method=data_generation_methods,
586
- correlation_id=correlation_id,
587
- )
588
- hypothesis_output: list[str] = []
589
- errors: list[Exception] = []
590
- test_start_time = time.monotonic()
591
- setup_hypothesis_database_key(test, operation)
592
-
593
- def _on_flaky(exc: Exception) -> Status:
594
- if isinstance(exc.__cause__, hypothesis.errors.DeadlineExceeded):
595
- status = Status.error
596
- result.add_error(DeadlineExceeded.from_exc(exc.__cause__))
597
- elif (
598
- hasattr(hypothesis.errors, "FlakyFailure")
599
- and isinstance(exc, hypothesis.errors.FlakyFailure)
600
- and any(isinstance(subexc, hypothesis.errors.DeadlineExceeded) for subexc in exc.exceptions)
601
- ):
602
- for sub_exc in exc.exceptions:
603
- if isinstance(sub_exc, hypothesis.errors.DeadlineExceeded):
604
- result.add_error(DeadlineExceeded.from_exc(sub_exc))
605
- status = Status.error
606
- elif errors:
607
- status = Status.error
608
- add_errors(result, errors)
609
- else:
610
- status = Status.failure
611
- result.mark_flaky()
612
- return status
613
-
614
- try:
615
- with catch_warnings(record=True) as warnings, capture_hypothesis_output() as hypothesis_output:
616
- test(
617
- ctx=ctx,
618
- checks=checks,
619
- targets=targets,
620
- result=result,
621
- errors=errors,
622
- headers=headers,
623
- data_generation_methods=data_generation_methods,
624
- **kwargs,
625
- )
626
- # Test body was not executed at all - Hypothesis did not generate any tests, but there is no error
627
- if not result.is_executed:
628
- status = Status.skip
629
- result.mark_skipped(None)
630
- else:
631
- status = Status.success
632
- except unittest.case.SkipTest as exc:
633
- # Newer Hypothesis versions raise this exception if no tests were executed
634
- status = Status.skip
635
- result.mark_skipped(exc)
636
- except CheckFailed:
637
- status = Status.failure
638
- except NonCheckError:
639
- # It could be an error in user-defined extensions, network errors or internal Schemathesis errors
640
- status = Status.error
641
- result.mark_errored()
642
- for error in deduplicate_errors(errors):
643
- result.add_error(error)
644
- except hypothesis.errors.Flaky as exc:
645
- status = _on_flaky(exc)
646
- except MultipleFailures:
647
- # Schemathesis may detect multiple errors that come from different check results
648
- # They raise different "grouped" exceptions
649
- if errors:
650
- status = Status.error
651
- add_errors(result, errors)
652
- else:
653
- status = Status.failure
654
- except hypothesis.errors.Unsatisfiable:
655
- # We need more clear error message here
656
- status = Status.error
657
- result.add_error(hypothesis.errors.Unsatisfiable("Failed to generate test cases for this API operation"))
658
- except KeyboardInterrupt:
659
- yield events.Interrupted()
660
- return
661
- except SkipTest as exc:
662
- status = Status.skip
663
- result.mark_skipped(exc)
664
- except AssertionError as exc: # May come from `hypothesis-jsonschema` or `hypothesis`
665
- status = Status.error
666
- try:
667
- operation.schema.validate()
668
- msg = "Unexpected error during testing of this API operation"
669
- exc_msg = str(exc)
670
- if exc_msg:
671
- msg += f": {exc_msg}"
672
- try:
673
- raise InternalError(msg) from exc
674
- except InternalError as exc:
675
- error = exc
676
- except ValidationError as exc:
677
- error = OperationSchemaError.from_jsonschema_error(
678
- exc,
679
- path=operation.path,
680
- method=operation.method,
681
- full_path=operation.schema.get_full_path(operation.path),
682
- )
683
- result.add_error(error)
684
- except HypothesisRefResolutionError:
685
- status = Status.error
686
- result.add_error(RecursiveReferenceError(RECURSIVE_REFERENCE_ERROR_MESSAGE))
687
- except InvalidArgument as error:
688
- status = Status.error
689
- message = get_invalid_regular_expression_message(warnings)
690
- if message:
691
- # `hypothesis-jsonschema` emits a warning on invalid regular expression syntax
692
- result.add_error(InvalidRegularExpression.from_hypothesis_jsonschema_message(message))
693
- else:
694
- result.add_error(error)
695
- except hypothesis.errors.DeadlineExceeded as error:
696
- status = Status.error
697
- result.add_error(DeadlineExceeded.from_exc(error))
698
- except JsonSchemaError as error:
699
- status = Status.error
700
- result.add_error(InvalidRegularExpression.from_schema_error(error, from_examples=False))
701
- except Exception as error:
702
- status = Status.error
703
- # Likely a YAML parsing issue. E.g. `00:00:00.00` (without quotes) is parsed as float `0.0`
704
- if str(error) == "first argument must be string or compiled pattern":
705
- result.add_error(
706
- InvalidRegularExpression(
707
- "Invalid `pattern` value: expected a string. "
708
- "If your schema is in YAML, ensure `pattern` values are quoted",
709
- is_valid_type=False,
710
- )
711
- )
712
- else:
713
- result.add_error(error)
714
- if status == Status.success and ctx.no_failfast and any(check.value == Status.failure for check in result.checks):
715
- status = Status.failure
716
- if has_unsatisfied_example_mark(test):
717
- status = Status.error
718
- result.add_error(
719
- hypothesis.errors.Unsatisfiable("Failed to generate test cases from examples for this API operation")
720
- )
721
- non_serializable = get_non_serializable_mark(test)
722
- if non_serializable is not None and status != Status.error:
723
- status = Status.error
724
- media_types = ", ".join(non_serializable.media_types)
725
- result.add_error(
726
- SerializationNotPossible(
727
- "Failed to generate test cases from examples for this API operation because of"
728
- f" unsupported payload media types: {media_types}\n{SERIALIZERS_SUGGESTION_MESSAGE}",
729
- media_types=non_serializable.media_types,
730
- )
731
- )
732
- invalid_regex = get_invalid_regex_mark(test)
733
- if invalid_regex is not None and status != Status.error:
734
- status = Status.error
735
- result.add_error(InvalidRegularExpression.from_schema_error(invalid_regex, from_examples=True))
736
- invalid_headers = get_invalid_example_headers_mark(test)
737
- if invalid_headers:
738
- status = Status.error
739
- result.add_error(InvalidHeadersExample.from_headers(invalid_headers))
740
- test_elapsed_time = time.monotonic() - test_start_time
741
- # DEPRECATED: Seed is the same per test run
742
- # Fetch seed value, hypothesis generates it during test execution
743
- # It may be `None` if the `derandomize` config option is set to `True`
744
- result.seed = getattr(test, "_hypothesis_internal_use_seed", None) or getattr(
745
- test, "_hypothesis_internal_use_generated_seed", None
746
- )
747
- ctx.add_result(result)
748
- for status_code in (401, 403):
749
- if has_too_many_responses_with_status(result, status_code):
750
- ctx.add_warning(TOO_MANY_RESPONSES_WARNING_TEMPLATE.format(f"`{operation.verbose_name}`", status_code))
751
- yield events.AfterExecution.from_result(
752
- result=result,
753
- status=status,
754
- elapsed_time=test_elapsed_time,
755
- hypothesis_output=hypothesis_output,
756
- operation=operation,
757
- data_generation_method=data_generation_methods,
758
- correlation_id=correlation_id,
759
- )
760
-
761
-
762
- TOO_MANY_RESPONSES_WARNING_TEMPLATE = (
763
- "Most of the responses from {} have a {} status code. Did you specify proper API credentials?"
764
- )
765
- TOO_MANY_RESPONSES_THRESHOLD = 0.9
766
-
767
-
768
- def has_too_many_responses_with_status(result: TestResult, status_code: int) -> bool:
769
- # It is faster than creating an intermediate list
770
- unauthorized_count = 0
771
- total = 0
772
- for check in result.checks:
773
- if check.response is not None:
774
- if check.response.status_code == status_code:
775
- unauthorized_count += 1
776
- total += 1
777
- if not total:
778
- return False
779
- return unauthorized_count / total >= TOO_MANY_RESPONSES_THRESHOLD
780
-
781
-
782
- def setup_hypothesis_database_key(test: Callable, operation: APIOperation) -> None:
783
- """Make Hypothesis use separate database entries for every API operation.
784
-
785
- It increases the effectiveness of the Hypothesis database in the CLI.
786
- """
787
- # Hypothesis's function digest depends on the test function signature. To reflect it for the web API case,
788
- # we use all API operation parameters in the digest.
789
- extra = operation.verbose_name.encode("utf8")
790
- for parameter in operation.iter_parameters():
791
- extra += parameter.serialize(operation).encode("utf8")
792
- test.hypothesis.inner_test._hypothesis_internal_add_digest = extra # type: ignore
793
-
794
-
795
- def get_invalid_regular_expression_message(warnings: list[WarningMessage]) -> str | None:
796
- for warning in warnings:
797
- message = str(warning.message)
798
- if "is not valid syntax for a Python regular expression" in message:
799
- return message
800
- return None
801
-
802
-
803
- MEMORY_ADDRESS_RE = re.compile("0x[0-9a-fA-F]+")
804
- URL_IN_ERROR_MESSAGE_RE = re.compile(r"Max retries exceeded with url: .*? \(Caused by")
805
-
806
-
807
- def add_errors(result: TestResult, errors: list[Exception]) -> None:
808
- group_errors(errors)
809
- for error in deduplicate_errors(errors):
810
- result.add_error(error)
811
-
812
-
813
- def group_errors(errors: list[Exception]) -> None:
814
- """Group errors of the same kind info a single one, avoiding duplicate error messages."""
815
- serialization_errors = [error for error in errors if isinstance(error, SerializationNotPossible)]
816
- if len(serialization_errors) > 1:
817
- errors[:] = [error for error in errors if not isinstance(error, SerializationNotPossible)]
818
- media_types: list[str] = functools.reduce(
819
- operator.iadd, (entry.media_types for entry in serialization_errors), []
820
- )
821
- errors.append(SerializationNotPossible.from_media_types(*media_types))
822
-
823
-
824
- def canonicalize_error_message(error: Exception, include_traceback: bool = True) -> str:
825
- message = format_exception(error, include_traceback)
826
- # Replace memory addresses with a fixed string
827
- message = MEMORY_ADDRESS_RE.sub("0xbaaaaaaaaaad", message)
828
- return URL_IN_ERROR_MESSAGE_RE.sub("", message)
829
-
830
-
831
- def deduplicate_errors(errors: list[Exception]) -> Generator[Exception, None, None]:
832
- """Deduplicate errors by their messages + tracebacks."""
833
- seen = set()
834
- for error in errors:
835
- message = canonicalize_error_message(error)
836
- if message in seen:
837
- continue
838
- seen.add(message)
839
- yield error
840
-
841
-
842
- def run_checks(
843
- *,
844
- case: Case,
845
- ctx: CheckContext,
846
- checks: Iterable[CheckFunction],
847
- check_results: list[Check],
848
- result: TestResult,
849
- response: GenericResponse,
850
- elapsed_time: float,
851
- max_response_time: int | None = None,
852
- no_failfast: bool,
853
- ) -> None:
854
- errors = []
855
-
856
- def add_single_failure(error: AssertionError) -> None:
857
- msg = maybe_set_assertion_message(error, check_name)
858
- errors.append(error)
859
- if isinstance(error, CheckFailed):
860
- context = error.context
861
- else:
862
- context = None
863
- check_results.append(result.add_failure(check_name, copied_case, response, elapsed_time, msg, context))
864
-
865
- for check in checks:
866
- check_name = check.__name__
867
- copied_case = case.partial_deepcopy()
868
- try:
869
- skip_check = check(ctx, response, copied_case)
870
- if not skip_check:
871
- check_result = result.add_success(check_name, copied_case, response, elapsed_time)
872
- check_results.append(check_result)
873
- except AssertionError as exc:
874
- add_single_failure(exc)
875
- except MultipleFailures as exc:
876
- for exception in exc.exceptions:
877
- add_single_failure(exception)
878
-
879
- if max_response_time:
880
- if elapsed_time > max_response_time:
881
- message = _make_max_response_time_failure_message(elapsed_time, max_response_time)
882
- errors.append(AssertionError(message))
883
- result.add_failure(
884
- "max_response_time",
885
- case,
886
- response,
887
- elapsed_time,
888
- message,
889
- failures.ResponseTimeExceeded(message=message, elapsed=elapsed_time, deadline=max_response_time),
890
- )
891
- else:
892
- result.add_success("max_response_time", case, response, elapsed_time)
893
-
894
- if errors and not no_failfast:
895
- raise get_grouped_exception(case.operation.verbose_name, *errors)(causes=tuple(errors))
896
-
897
-
898
- def run_targets(targets: Iterable[Callable], context: TargetContext) -> None:
899
- for target in targets:
900
- value = target(context)
901
- hypothesis.target(value, label=target.__name__)
902
-
903
-
904
- def add_cases(case: Case, response: GenericResponse, test: Callable, *args: Any) -> None:
905
- context = HookContext(case.operation)
906
- for case_hook in get_all_by_name("add_case"):
907
- _case = case_hook(context, case.partial_deepcopy(), response)
908
- # run additional test if _case is not an empty value
909
- if _case:
910
- test(_case, *args)
911
-
912
-
913
- @dataclass
914
- class ErrorCollector:
915
- """Collect exceptions that are not related to failed checks.
916
-
917
- Such exceptions may be considered as multiple failures or flakiness by Hypothesis. In both cases, Hypothesis hides
918
- exception information that, in our case, is helpful for the end-user. It either indicates errors in user-defined
919
- extensions, network-related errors, or internal Schemathesis errors. In all cases, this information is useful for
920
- debugging.
921
-
922
- To mitigate this, we gather all exceptions manually via this context manager to avoid interfering with the test
923
- function signatures, which are used by Hypothesis.
924
- """
925
-
926
- errors: list[Exception]
927
-
928
- def __enter__(self) -> ErrorCollector:
929
- return self
930
-
931
- def __exit__(
932
- self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None
933
- ) -> Literal[False]:
934
- # Don't do anything special if:
935
- # - Tests are successful
936
- # - Checks failed
937
- # - The testing process is interrupted
938
- if not exc_type or issubclass(exc_type, CheckFailed) or not issubclass(exc_type, Exception):
939
- return False
940
- # These exceptions are needed for control flow on the Hypothesis side. E.g. rejecting unsatisfiable examples
941
- if isinstance(exc_val, HypothesisException):
942
- raise
943
- # Exception value is not `None` and is a subclass of `Exception` at this point
944
- exc_val = cast(Exception, exc_val)
945
- self.errors.append(exc_val.with_traceback(exc_tb))
946
- raise NonCheckError from None
947
-
948
-
949
- def _force_data_generation_method(values: list[DataGenerationMethod], case: Case) -> None:
950
- # Set data generation method to the one that actually used
951
- data_generation_method = cast(DataGenerationMethod, case.data_generation_method)
952
- values[:] = [data_generation_method]
953
-
954
-
955
- def cached_test_func(f: Callable) -> Callable:
956
- def wrapped(*, ctx: RunnerContext, case: Case, **kwargs: Any) -> None:
957
- if ctx.unique_data:
958
- cached = ctx.get_cached_outcome(case)
959
- if isinstance(cached, BaseException):
960
- raise cached
961
- elif cached is None:
962
- return None
963
- try:
964
- f(ctx=ctx, case=case, **kwargs)
965
- except BaseException as exc:
966
- ctx.cache_outcome(case, exc)
967
- raise
968
- else:
969
- ctx.cache_outcome(case, None)
970
- else:
971
- f(ctx=ctx, case=case, **kwargs)
972
-
973
- wrapped.__name__ = f.__name__
974
-
975
- return wrapped
976
-
977
-
978
- @cached_test_func
979
- def network_test(
980
- *,
981
- ctx: RunnerContext,
982
- case: Case,
983
- checks: Iterable[CheckFunction],
984
- targets: Iterable[Target],
985
- result: TestResult,
986
- session: requests.Session,
987
- request_config: RequestConfig,
988
- store_interactions: bool,
989
- headers: dict[str, Any] | None,
990
- feedback: Feedback | None,
991
- max_response_time: int | None,
992
- data_generation_methods: list[DataGenerationMethod],
993
- dry_run: bool,
994
- errors: list[Exception],
995
- ) -> None:
996
- """A single test body will be executed against the target."""
997
- with ErrorCollector(errors):
998
- _force_data_generation_method(data_generation_methods, case)
999
- result.mark_executed()
1000
- headers = headers or {}
1001
- if "user-agent" not in {header.lower() for header in headers}:
1002
- headers["User-Agent"] = USER_AGENT
1003
- if not dry_run:
1004
- args = (
1005
- ctx,
1006
- checks,
1007
- targets,
1008
- result,
1009
- session,
1010
- request_config,
1011
- store_interactions,
1012
- headers,
1013
- feedback,
1014
- max_response_time,
1015
- )
1016
- response = _network_test(case, *args)
1017
- add_cases(case, response, _network_test, *args)
1018
- elif store_interactions:
1019
- result.store_requests_response(case, None, Status.skip, [], headers=headers, session=session)
1020
-
1021
-
1022
- def _network_test(
1023
- case: Case,
1024
- ctx: RunnerContext,
1025
- checks: Iterable[CheckFunction],
1026
- targets: Iterable[Target],
1027
- result: TestResult,
1028
- session: requests.Session,
1029
- request_config: RequestConfig,
1030
- store_interactions: bool,
1031
- headers: dict[str, Any] | None,
1032
- feedback: Feedback | None,
1033
- max_response_time: int | None,
1034
- ) -> requests.Response:
1035
- check_results: list[Check] = []
1036
- hook_context = HookContext(operation=case.operation)
1037
- kwargs: dict[str, Any] = {
1038
- "session": session,
1039
- "headers": headers,
1040
- "timeout": request_config.prepared_timeout,
1041
- "verify": request_config.tls_verify,
1042
- "cert": request_config.cert,
1043
- }
1044
- if request_config.proxy is not None:
1045
- kwargs["proxies"] = {"all": request_config.proxy}
1046
- hooks.dispatch("process_call_kwargs", hook_context, case, kwargs)
1047
- try:
1048
- response = case.call(**kwargs)
1049
- except CheckFailed as exc:
1050
- check_name = "request_timeout"
1051
- requests_kwargs = RequestsTransport().serialize_case(case, base_url=case.get_full_base_url(), headers=headers)
1052
- request = requests.Request(**requests_kwargs).prepare()
1053
- elapsed = cast(
1054
- float, request_config.prepared_timeout
1055
- ) # It is defined and not empty, since the exception happened
1056
- check_result = result.add_failure(
1057
- check_name, case, None, elapsed, f"Response timed out after {1000 * elapsed:.2f}ms", exc.context, request
1058
- )
1059
- check_results.append(check_result)
1060
- if store_interactions:
1061
- result.store_requests_response(case, None, Status.failure, [check_result], headers=headers, session=session)
1062
- raise exc
1063
- context = TargetContext(case=case, response=response, response_time=response.elapsed.total_seconds())
1064
- run_targets(targets, context)
1065
- status = Status.success
1066
-
1067
- check_ctx = CheckContext(
1068
- override=ctx.override,
1069
- auth=ctx.auth,
1070
- headers=CaseInsensitiveDict(headers) if headers else None,
1071
- config=ctx.checks_config,
1072
- transport_kwargs=kwargs,
1073
- )
1074
- try:
1075
- run_checks(
1076
- case=case,
1077
- ctx=check_ctx,
1078
- checks=checks,
1079
- check_results=check_results,
1080
- result=result,
1081
- response=response,
1082
- elapsed_time=context.response_time * 1000,
1083
- max_response_time=max_response_time,
1084
- no_failfast=ctx.no_failfast,
1085
- )
1086
- except CheckFailed:
1087
- status = Status.failure
1088
- raise
1089
- finally:
1090
- if feedback is not None:
1091
- feedback.add_test_case(case, response)
1092
- if store_interactions:
1093
- result.store_requests_response(case, response, status, check_results, headers=headers, session=session)
1094
- return response
1095
-
1096
-
1097
- @contextmanager
1098
- def get_session(auth: HTTPDigestAuth | RawAuth | None = None) -> Generator[requests.Session, None, None]:
1099
- with requests.Session() as session:
1100
- if auth is not None:
1101
- session.auth = auth
1102
- yield session
1103
-
1104
-
1105
- @cached_test_func
1106
- def wsgi_test(
1107
- ctx: RunnerContext,
1108
- case: Case,
1109
- checks: Iterable[CheckFunction],
1110
- targets: Iterable[Target],
1111
- result: TestResult,
1112
- auth: RawAuth | None,
1113
- auth_type: str | None,
1114
- headers: dict[str, Any] | None,
1115
- store_interactions: bool,
1116
- feedback: Feedback | None,
1117
- max_response_time: int | None,
1118
- data_generation_methods: list[DataGenerationMethod],
1119
- dry_run: bool,
1120
- errors: list[Exception],
1121
- ) -> None:
1122
- with ErrorCollector(errors):
1123
- _force_data_generation_method(data_generation_methods, case)
1124
- result.mark_executed()
1125
- headers = prepare_wsgi_headers(headers, auth, auth_type)
1126
- if not dry_run:
1127
- args = (
1128
- ctx,
1129
- checks,
1130
- targets,
1131
- result,
1132
- headers,
1133
- store_interactions,
1134
- feedback,
1135
- max_response_time,
1136
- )
1137
- response = _wsgi_test(case, *args)
1138
- add_cases(case, response, _wsgi_test, *args)
1139
- elif store_interactions:
1140
- result.store_wsgi_response(case, None, headers, None, Status.skip, [])
1141
-
1142
-
1143
- def _wsgi_test(
1144
- case: Case,
1145
- ctx: RunnerContext,
1146
- checks: Iterable[CheckFunction],
1147
- targets: Iterable[Target],
1148
- result: TestResult,
1149
- headers: dict[str, Any],
1150
- store_interactions: bool,
1151
- feedback: Feedback | None,
1152
- max_response_time: int | None,
1153
- ) -> WSGIResponse:
1154
- from ...transports.responses import WSGIResponse
1155
-
1156
- with catching_logs(LogCaptureHandler(), level=logging.DEBUG) as recorded:
1157
- hook_context = HookContext(operation=case.operation)
1158
- kwargs: dict[str, Any] = {"headers": headers}
1159
- hooks.dispatch("process_call_kwargs", hook_context, case, kwargs)
1160
- response = cast(WSGIResponse, case.call(**kwargs))
1161
- context = TargetContext(case=case, response=response, response_time=response.elapsed.total_seconds())
1162
- run_targets(targets, context)
1163
- result.logs.extend(recorded.records)
1164
- status = Status.success
1165
- check_results: list[Check] = []
1166
- check_ctx = CheckContext(
1167
- override=ctx.override,
1168
- auth=ctx.auth,
1169
- headers=CaseInsensitiveDict(headers) if headers else None,
1170
- config=ctx.checks_config,
1171
- transport_kwargs=kwargs,
1172
- )
1173
- try:
1174
- run_checks(
1175
- case=case,
1176
- ctx=check_ctx,
1177
- checks=checks,
1178
- check_results=check_results,
1179
- result=result,
1180
- response=response,
1181
- elapsed_time=context.response_time * 1000,
1182
- max_response_time=max_response_time,
1183
- no_failfast=ctx.no_failfast,
1184
- )
1185
- except CheckFailed:
1186
- status = Status.failure
1187
- raise
1188
- finally:
1189
- if feedback is not None:
1190
- feedback.add_test_case(case, response)
1191
- if store_interactions:
1192
- result.store_wsgi_response(case, response, headers, response.elapsed.total_seconds(), status, check_results)
1193
- return response
1194
-
1195
-
1196
- @cached_test_func
1197
- def asgi_test(
1198
- ctx: RunnerContext,
1199
- case: Case,
1200
- checks: Iterable[CheckFunction],
1201
- targets: Iterable[Target],
1202
- result: TestResult,
1203
- store_interactions: bool,
1204
- headers: dict[str, Any] | None,
1205
- feedback: Feedback | None,
1206
- max_response_time: int | None,
1207
- data_generation_methods: list[DataGenerationMethod],
1208
- dry_run: bool,
1209
- errors: list[Exception],
1210
- ) -> None:
1211
- """A single test body will be executed against the target."""
1212
- with ErrorCollector(errors):
1213
- _force_data_generation_method(data_generation_methods, case)
1214
- result.mark_executed()
1215
- headers = headers or {}
1216
-
1217
- if not dry_run:
1218
- args = (
1219
- ctx,
1220
- checks,
1221
- targets,
1222
- result,
1223
- store_interactions,
1224
- headers,
1225
- feedback,
1226
- max_response_time,
1227
- )
1228
- response = _asgi_test(case, *args)
1229
- add_cases(case, response, _asgi_test, *args)
1230
- elif store_interactions:
1231
- result.store_requests_response(case, None, Status.skip, [], headers=headers, session=None)
1232
-
1233
-
1234
- def _asgi_test(
1235
- case: Case,
1236
- ctx: RunnerContext,
1237
- checks: Iterable[CheckFunction],
1238
- targets: Iterable[Target],
1239
- result: TestResult,
1240
- store_interactions: bool,
1241
- headers: dict[str, Any] | None,
1242
- feedback: Feedback | None,
1243
- max_response_time: int | None,
1244
- ) -> requests.Response:
1245
- hook_context = HookContext(operation=case.operation)
1246
- kwargs: dict[str, Any] = {"headers": headers}
1247
- hooks.dispatch("process_call_kwargs", hook_context, case, kwargs)
1248
- response = case.call(**kwargs)
1249
- context = TargetContext(case=case, response=response, response_time=response.elapsed.total_seconds())
1250
- run_targets(targets, context)
1251
- status = Status.success
1252
- check_results: list[Check] = []
1253
- check_ctx = CheckContext(
1254
- override=ctx.override,
1255
- auth=ctx.auth,
1256
- headers=CaseInsensitiveDict(headers) if headers else None,
1257
- config=ctx.checks_config,
1258
- transport_kwargs=kwargs,
1259
- )
1260
- try:
1261
- run_checks(
1262
- case=case,
1263
- ctx=check_ctx,
1264
- checks=checks,
1265
- check_results=check_results,
1266
- result=result,
1267
- response=response,
1268
- elapsed_time=context.response_time * 1000,
1269
- max_response_time=max_response_time,
1270
- no_failfast=ctx.no_failfast,
1271
- )
1272
- except CheckFailed:
1273
- status = Status.failure
1274
- raise
1275
- finally:
1276
- if feedback is not None:
1277
- feedback.add_test_case(case, response)
1278
- if store_interactions:
1279
- result.store_requests_response(case, response, status, check_results, headers, session=None)
1280
- return response