schemathesis 3.25.6__py3-none-any.whl → 4.0.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +102 -82
  3. schemathesis/checks.py +126 -46
  4. schemathesis/cli/__init__.py +11 -1760
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +37 -0
  7. schemathesis/cli/commands/run/__init__.py +662 -0
  8. schemathesis/cli/commands/run/checks.py +80 -0
  9. schemathesis/cli/commands/run/context.py +117 -0
  10. schemathesis/cli/commands/run/events.py +35 -0
  11. schemathesis/cli/commands/run/executor.py +138 -0
  12. schemathesis/cli/commands/run/filters.py +194 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +18 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  17. schemathesis/cli/commands/run/handlers/output.py +746 -0
  18. schemathesis/cli/commands/run/hypothesis.py +105 -0
  19. schemathesis/cli/commands/run/loaders.py +129 -0
  20. schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
  21. schemathesis/cli/constants.py +5 -52
  22. schemathesis/cli/core.py +17 -0
  23. schemathesis/cli/ext/fs.py +14 -0
  24. schemathesis/cli/ext/groups.py +55 -0
  25. schemathesis/cli/{options.py → ext/options.py} +39 -10
  26. schemathesis/cli/hooks.py +36 -0
  27. schemathesis/contrib/__init__.py +1 -3
  28. schemathesis/contrib/openapi/__init__.py +1 -3
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
  30. schemathesis/core/__init__.py +58 -0
  31. schemathesis/core/compat.py +25 -0
  32. schemathesis/core/control.py +2 -0
  33. schemathesis/core/curl.py +58 -0
  34. schemathesis/core/deserialization.py +65 -0
  35. schemathesis/core/errors.py +370 -0
  36. schemathesis/core/failures.py +285 -0
  37. schemathesis/core/fs.py +19 -0
  38. schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
  39. schemathesis/core/loaders.py +104 -0
  40. schemathesis/core/marks.py +66 -0
  41. schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
  42. schemathesis/core/output/__init__.py +69 -0
  43. schemathesis/core/output/sanitization.py +197 -0
  44. schemathesis/core/rate_limit.py +60 -0
  45. schemathesis/core/registries.py +31 -0
  46. schemathesis/{internal → core}/result.py +1 -1
  47. schemathesis/core/transforms.py +113 -0
  48. schemathesis/core/transport.py +108 -0
  49. schemathesis/core/validation.py +38 -0
  50. schemathesis/core/version.py +7 -0
  51. schemathesis/engine/__init__.py +30 -0
  52. schemathesis/engine/config.py +59 -0
  53. schemathesis/engine/context.py +119 -0
  54. schemathesis/engine/control.py +36 -0
  55. schemathesis/engine/core.py +157 -0
  56. schemathesis/engine/errors.py +394 -0
  57. schemathesis/engine/events.py +337 -0
  58. schemathesis/engine/phases/__init__.py +66 -0
  59. schemathesis/{runner → engine/phases}/probes.py +50 -67
  60. schemathesis/engine/phases/stateful/__init__.py +65 -0
  61. schemathesis/engine/phases/stateful/_executor.py +326 -0
  62. schemathesis/engine/phases/stateful/context.py +85 -0
  63. schemathesis/engine/phases/unit/__init__.py +174 -0
  64. schemathesis/engine/phases/unit/_executor.py +321 -0
  65. schemathesis/engine/phases/unit/_pool.py +74 -0
  66. schemathesis/engine/recorder.py +241 -0
  67. schemathesis/errors.py +31 -0
  68. schemathesis/experimental/__init__.py +18 -14
  69. schemathesis/filters.py +103 -14
  70. schemathesis/generation/__init__.py +21 -37
  71. schemathesis/generation/case.py +190 -0
  72. schemathesis/generation/coverage.py +931 -0
  73. schemathesis/generation/hypothesis/__init__.py +30 -0
  74. schemathesis/generation/hypothesis/builder.py +585 -0
  75. schemathesis/generation/hypothesis/examples.py +50 -0
  76. schemathesis/generation/hypothesis/given.py +66 -0
  77. schemathesis/generation/hypothesis/reporting.py +14 -0
  78. schemathesis/generation/hypothesis/strategies.py +16 -0
  79. schemathesis/generation/meta.py +115 -0
  80. schemathesis/generation/modes.py +28 -0
  81. schemathesis/generation/overrides.py +96 -0
  82. schemathesis/generation/stateful/__init__.py +20 -0
  83. schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
  84. schemathesis/generation/targets.py +69 -0
  85. schemathesis/graphql/__init__.py +15 -0
  86. schemathesis/graphql/checks.py +115 -0
  87. schemathesis/graphql/loaders.py +131 -0
  88. schemathesis/hooks.py +99 -67
  89. schemathesis/openapi/__init__.py +13 -0
  90. schemathesis/openapi/checks.py +412 -0
  91. schemathesis/openapi/generation/__init__.py +0 -0
  92. schemathesis/openapi/generation/filters.py +63 -0
  93. schemathesis/openapi/loaders.py +178 -0
  94. schemathesis/pytest/__init__.py +5 -0
  95. schemathesis/pytest/control_flow.py +7 -0
  96. schemathesis/pytest/lazy.py +273 -0
  97. schemathesis/pytest/loaders.py +12 -0
  98. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
  99. schemathesis/python/__init__.py +0 -0
  100. schemathesis/python/asgi.py +12 -0
  101. schemathesis/python/wsgi.py +12 -0
  102. schemathesis/schemas.py +537 -261
  103. schemathesis/specs/graphql/__init__.py +0 -1
  104. schemathesis/specs/graphql/_cache.py +25 -0
  105. schemathesis/specs/graphql/nodes.py +1 -0
  106. schemathesis/specs/graphql/scalars.py +7 -5
  107. schemathesis/specs/graphql/schemas.py +215 -187
  108. schemathesis/specs/graphql/validation.py +11 -18
  109. schemathesis/specs/openapi/__init__.py +7 -1
  110. schemathesis/specs/openapi/_cache.py +122 -0
  111. schemathesis/specs/openapi/_hypothesis.py +146 -165
  112. schemathesis/specs/openapi/checks.py +565 -67
  113. schemathesis/specs/openapi/converter.py +33 -6
  114. schemathesis/specs/openapi/definitions.py +11 -18
  115. schemathesis/specs/openapi/examples.py +139 -23
  116. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  117. schemathesis/specs/openapi/expressions/context.py +4 -6
  118. schemathesis/specs/openapi/expressions/extractors.py +23 -0
  119. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  120. schemathesis/specs/openapi/expressions/nodes.py +38 -14
  121. schemathesis/specs/openapi/expressions/parser.py +26 -5
  122. schemathesis/specs/openapi/formats.py +45 -0
  123. schemathesis/specs/openapi/links.py +65 -165
  124. schemathesis/specs/openapi/media_types.py +32 -0
  125. schemathesis/specs/openapi/negative/__init__.py +7 -3
  126. schemathesis/specs/openapi/negative/mutations.py +24 -8
  127. schemathesis/specs/openapi/parameters.py +46 -30
  128. schemathesis/specs/openapi/patterns.py +137 -0
  129. schemathesis/specs/openapi/references.py +47 -57
  130. schemathesis/specs/openapi/schemas.py +478 -369
  131. schemathesis/specs/openapi/security.py +25 -7
  132. schemathesis/specs/openapi/serialization.py +11 -6
  133. schemathesis/specs/openapi/stateful/__init__.py +185 -73
  134. schemathesis/specs/openapi/utils.py +6 -1
  135. schemathesis/transport/__init__.py +104 -0
  136. schemathesis/transport/asgi.py +26 -0
  137. schemathesis/transport/prepare.py +99 -0
  138. schemathesis/transport/requests.py +221 -0
  139. schemathesis/{_xml.py → transport/serialization.py} +143 -28
  140. schemathesis/transport/wsgi.py +165 -0
  141. schemathesis-4.0.0a1.dist-info/METADATA +297 -0
  142. schemathesis-4.0.0a1.dist-info/RECORD +152 -0
  143. {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
  144. {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
  145. schemathesis/_compat.py +0 -74
  146. schemathesis/_dependency_versions.py +0 -17
  147. schemathesis/_hypothesis.py +0 -246
  148. schemathesis/_override.py +0 -49
  149. schemathesis/cli/cassettes.py +0 -375
  150. schemathesis/cli/context.py +0 -58
  151. schemathesis/cli/debug.py +0 -26
  152. schemathesis/cli/handlers.py +0 -16
  153. schemathesis/cli/junitxml.py +0 -43
  154. schemathesis/cli/output/__init__.py +0 -1
  155. schemathesis/cli/output/default.py +0 -790
  156. schemathesis/cli/output/short.py +0 -44
  157. schemathesis/cli/sanitization.py +0 -20
  158. schemathesis/code_samples.py +0 -149
  159. schemathesis/constants.py +0 -55
  160. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  161. schemathesis/contrib/openapi/formats/uuid.py +0 -15
  162. schemathesis/contrib/unique_data.py +0 -41
  163. schemathesis/exceptions.py +0 -560
  164. schemathesis/extra/_aiohttp.py +0 -27
  165. schemathesis/extra/_flask.py +0 -10
  166. schemathesis/extra/_server.py +0 -17
  167. schemathesis/failures.py +0 -209
  168. schemathesis/fixups/__init__.py +0 -36
  169. schemathesis/fixups/fast_api.py +0 -41
  170. schemathesis/fixups/utf8_bom.py +0 -29
  171. schemathesis/graphql.py +0 -4
  172. schemathesis/internal/__init__.py +0 -7
  173. schemathesis/internal/copy.py +0 -13
  174. schemathesis/internal/datetime.py +0 -5
  175. schemathesis/internal/deprecation.py +0 -34
  176. schemathesis/internal/jsonschema.py +0 -35
  177. schemathesis/internal/transformation.py +0 -15
  178. schemathesis/internal/validation.py +0 -34
  179. schemathesis/lazy.py +0 -361
  180. schemathesis/loaders.py +0 -120
  181. schemathesis/models.py +0 -1234
  182. schemathesis/parameters.py +0 -86
  183. schemathesis/runner/__init__.py +0 -570
  184. schemathesis/runner/events.py +0 -329
  185. schemathesis/runner/impl/__init__.py +0 -3
  186. schemathesis/runner/impl/core.py +0 -1035
  187. schemathesis/runner/impl/solo.py +0 -90
  188. schemathesis/runner/impl/threadpool.py +0 -400
  189. schemathesis/runner/serialization.py +0 -411
  190. schemathesis/sanitization.py +0 -248
  191. schemathesis/serializers.py +0 -323
  192. schemathesis/service/__init__.py +0 -18
  193. schemathesis/service/auth.py +0 -11
  194. schemathesis/service/ci.py +0 -201
  195. schemathesis/service/client.py +0 -100
  196. schemathesis/service/constants.py +0 -38
  197. schemathesis/service/events.py +0 -57
  198. schemathesis/service/hosts.py +0 -107
  199. schemathesis/service/metadata.py +0 -46
  200. schemathesis/service/models.py +0 -49
  201. schemathesis/service/report.py +0 -255
  202. schemathesis/service/serialization.py +0 -199
  203. schemathesis/service/usage.py +0 -65
  204. schemathesis/specs/graphql/loaders.py +0 -344
  205. schemathesis/specs/openapi/filters.py +0 -49
  206. schemathesis/specs/openapi/loaders.py +0 -667
  207. schemathesis/specs/openapi/stateful/links.py +0 -92
  208. schemathesis/specs/openapi/validation.py +0 -25
  209. schemathesis/stateful/__init__.py +0 -133
  210. schemathesis/targets.py +0 -45
  211. schemathesis/throttling.py +0 -41
  212. schemathesis/transports/__init__.py +0 -5
  213. schemathesis/transports/auth.py +0 -15
  214. schemathesis/transports/headers.py +0 -35
  215. schemathesis/transports/responses.py +0 -52
  216. schemathesis/types.py +0 -35
  217. schemathesis/utils.py +0 -169
  218. schemathesis-3.25.6.dist-info/METADATA +0 -356
  219. schemathesis-3.25.6.dist-info/RECORD +0 -134
  220. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  221. {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -1,1035 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import logging
4
- import re
5
- import threading
6
- import time
7
- import unittest
8
- import uuid
9
- from contextlib import contextmanager
10
- from dataclasses import dataclass, field
11
- from types import TracebackType
12
- from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, List, Literal, cast
13
- from warnings import WarningMessage, catch_warnings
14
-
15
- import hypothesis
16
- import requests
17
- from _pytest.logging import LogCaptureHandler, catching_logs
18
- from hypothesis.errors import HypothesisException, InvalidArgument
19
- from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
20
- from jsonschema.exceptions import SchemaError as JsonSchemaError
21
- from jsonschema.exceptions import ValidationError
22
- from requests.auth import HTTPDigestAuth, _basic_auth_str
23
-
24
- from ... import failures, hooks
25
- from ..._compat import MultipleFailures
26
- from ..._hypothesis import (
27
- get_invalid_example_headers_mark,
28
- get_invalid_regex_mark,
29
- get_non_serializable_mark,
30
- has_unsatisfied_example_mark,
31
- )
32
- from ..._override import CaseOverride
33
- from ...auths import unregister as unregister_auth
34
- from ...constants import (
35
- DEFAULT_STATEFUL_RECURSION_LIMIT,
36
- RECURSIVE_REFERENCE_ERROR_MESSAGE,
37
- SERIALIZERS_SUGGESTION_MESSAGE,
38
- USER_AGENT,
39
- )
40
- from ...exceptions import (
41
- CheckFailed,
42
- DeadlineExceeded,
43
- InvalidHeadersExample,
44
- InvalidRegularExpression,
45
- NonCheckError,
46
- OperationSchemaError,
47
- SerializationNotPossible,
48
- SkipTest,
49
- format_exception,
50
- get_grouped_exception,
51
- maybe_set_assertion_message,
52
- )
53
- from ...generation import DataGenerationMethod, GenerationConfig
54
- from ...hooks import HookContext, get_all_by_name
55
- from ...internal.datetime import current_datetime
56
- from ...internal.result import Ok
57
- from ...models import APIOperation, Case, Check, CheckFunction, Status, TestResult, TestResultSet
58
- from ...runner import events
59
- from ...schemas import BaseSchema
60
- from ...specs.openapi import formats
61
- from ...stateful import Feedback, Stateful
62
- from ...targets import Target, TargetContext
63
- from ...types import RawAuth, RequestCert
64
- from ...utils import capture_hypothesis_output
65
- from .. import probes
66
- from ..serialization import SerializedTestResult
67
-
68
- if TYPE_CHECKING:
69
- from ...transports.responses import GenericResponse, WSGIResponse
70
-
71
-
72
- def _should_count_towards_stop(event: events.ExecutionEvent) -> bool:
73
- return isinstance(event, events.AfterExecution) and event.status in (Status.error, Status.failure)
74
-
75
-
76
- @dataclass
77
- class BaseRunner:
78
- schema: BaseSchema
79
- checks: Iterable[CheckFunction]
80
- max_response_time: int | None
81
- targets: Iterable[Target]
82
- hypothesis_settings: hypothesis.settings
83
- generation_config: GenerationConfig
84
- probe_config: probes.ProbeConfig
85
- override: CaseOverride | None = None
86
- auth: RawAuth | None = None
87
- auth_type: str | None = None
88
- headers: dict[str, Any] | None = None
89
- request_timeout: int | None = None
90
- store_interactions: bool = False
91
- seed: int | None = None
92
- exit_first: bool = False
93
- max_failures: int | None = None
94
- started_at: str = field(default_factory=current_datetime)
95
- dry_run: bool = False
96
- stateful: Stateful | None = None
97
- stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT
98
- count_operations: bool = True
99
- count_links: bool = True
100
- _failures_counter: int = 0
101
-
102
- def execute(self) -> EventStream:
103
- """Common logic for all runners."""
104
- event = threading.Event()
105
- return EventStream(self._generate_events(event), event)
106
-
107
- def _generate_events(self, stop_event: threading.Event) -> Generator[events.ExecutionEvent, None, None]:
108
- # If auth is explicitly provided, then the global provider is ignored
109
- if self.auth is not None:
110
- unregister_auth()
111
- results = TestResultSet(seed=self.seed)
112
- initialized = None
113
- __probes = None
114
- start_time = time.monotonic()
115
-
116
- def _initialize() -> events.Initialized:
117
- nonlocal initialized
118
- initialized = events.Initialized.from_schema(
119
- schema=self.schema,
120
- count_operations=self.count_operations,
121
- count_links=self.count_links,
122
- seed=self.seed,
123
- start_time=start_time,
124
- )
125
- return initialized
126
-
127
- def _finish() -> events.Finished:
128
- if has_all_not_found(results):
129
- results.add_warning(ALL_NOT_FOUND_WARNING_MESSAGE)
130
- return events.Finished.from_results(results=results, running_time=time.monotonic() - start_time)
131
-
132
- def _before_probes() -> events.BeforeProbing:
133
- return events.BeforeProbing()
134
-
135
- def _run_probes() -> None:
136
- if not self.dry_run:
137
- nonlocal __probes
138
-
139
- __probes = run_probes(self.schema, self.probe_config)
140
-
141
- def _after_probes() -> events.AfterProbing:
142
- _probes = cast(List[probes.ProbeRun], __probes)
143
- return events.AfterProbing(probes=_probes)
144
-
145
- if stop_event.is_set():
146
- yield _finish()
147
- return
148
-
149
- for event_factory in (
150
- _initialize,
151
- _before_probes,
152
- _run_probes,
153
- _after_probes,
154
- ):
155
- event = event_factory()
156
- if event is not None:
157
- yield event
158
- if stop_event.is_set():
159
- yield _finish()
160
- return
161
-
162
- try:
163
- yield from self._execute(results, stop_event)
164
- except KeyboardInterrupt:
165
- yield events.Interrupted()
166
-
167
- yield _finish()
168
-
169
- def _should_stop(self, event: events.ExecutionEvent) -> bool:
170
- if _should_count_towards_stop(event):
171
- if self.exit_first:
172
- return True
173
- if self.max_failures is not None:
174
- self._failures_counter += 1
175
- return self._failures_counter >= self.max_failures
176
- return False
177
-
178
- def _execute(
179
- self, results: TestResultSet, stop_event: threading.Event
180
- ) -> Generator[events.ExecutionEvent, None, None]:
181
- raise NotImplementedError
182
-
183
- def _run_tests(
184
- self,
185
- maker: Callable,
186
- template: Callable,
187
- settings: hypothesis.settings,
188
- generation_config: GenerationConfig,
189
- seed: int | None,
190
- results: TestResultSet,
191
- recursion_level: int = 0,
192
- headers: dict[str, Any] | None = None,
193
- **kwargs: Any,
194
- ) -> Generator[events.ExecutionEvent, None, None]:
195
- """Run tests and recursively run additional tests."""
196
- if recursion_level > self.stateful_recursion_limit:
197
- return
198
-
199
- def as_strategy_kwargs(_operation: APIOperation) -> dict[str, Any]:
200
- kw = {}
201
- if self.override is not None:
202
- for location, entry in self.override.for_operation(_operation).items():
203
- if entry:
204
- kw[location] = entry
205
- if headers:
206
- kw["headers"] = {key: value for key, value in headers.items() if key.lower() != "user-agent"}
207
- return kw
208
-
209
- for result in maker(
210
- template,
211
- settings=settings,
212
- generation_config=generation_config,
213
- seed=seed,
214
- as_strategy_kwargs=as_strategy_kwargs,
215
- ):
216
- if isinstance(result, Ok):
217
- operation, test = result.ok()
218
- feedback = Feedback(self.stateful, operation)
219
- # Track whether `BeforeExecution` was already emitted.
220
- # Schema error may happen before / after `BeforeExecution`, but it should be emitted only once
221
- # and the `AfterExecution` event should have the same correlation id as previous `BeforeExecution`
222
- before_execution_correlation_id = None
223
- try:
224
- for event in run_test(
225
- operation,
226
- test,
227
- results=results,
228
- feedback=feedback,
229
- recursion_level=recursion_level,
230
- data_generation_methods=self.schema.data_generation_methods,
231
- headers=headers,
232
- **kwargs,
233
- ):
234
- yield event
235
- if isinstance(event, events.BeforeExecution):
236
- before_execution_correlation_id = event.correlation_id
237
- if isinstance(event, events.Interrupted):
238
- return
239
- # Additional tests, generated via the `feedback` instance
240
- yield from self._run_tests(
241
- feedback.get_stateful_tests,
242
- template,
243
- settings=settings,
244
- generation_config=generation_config,
245
- seed=seed,
246
- recursion_level=recursion_level + 1,
247
- results=results,
248
- headers=headers,
249
- **kwargs,
250
- )
251
- except OperationSchemaError as exc:
252
- yield from handle_schema_error(
253
- exc,
254
- results,
255
- self.schema.data_generation_methods,
256
- recursion_level,
257
- before_execution_correlation_id=before_execution_correlation_id,
258
- )
259
- else:
260
- # Schema errors
261
- yield from handle_schema_error(
262
- result.err(), results, self.schema.data_generation_methods, recursion_level
263
- )
264
-
265
-
266
- def run_probes(schema: BaseSchema, config: probes.ProbeConfig) -> list[probes.ProbeRun]:
267
- """Discover capabilities of the tested app."""
268
- results = probes.run(schema, config)
269
- for result in results:
270
- if isinstance(result.probe, probes.NullByteInHeader) and result.is_failure:
271
- from ...specs.openapi._hypothesis import HEADER_FORMAT, header_values
272
-
273
- formats.register(
274
- HEADER_FORMAT,
275
- header_values(blacklist_characters="\n\r\x00").map(str.lstrip),
276
- )
277
- return results
278
-
279
-
280
- @dataclass
281
- class EventStream:
282
- """Schemathesis event stream.
283
-
284
- Provides an API to control the execution flow.
285
- """
286
-
287
- generator: Generator[events.ExecutionEvent, None, None]
288
- stop_event: threading.Event
289
-
290
- def __next__(self) -> events.ExecutionEvent:
291
- return next(self.generator)
292
-
293
- def __iter__(self) -> Generator[events.ExecutionEvent, None, None]:
294
- return self.generator
295
-
296
- def stop(self) -> None:
297
- """Stop the event stream.
298
-
299
- Its next value will be the last one (Finished).
300
- """
301
- self.stop_event.set()
302
-
303
- def finish(self) -> events.ExecutionEvent:
304
- """Stop the event stream & return the last event."""
305
- self.stop()
306
- return next(self)
307
-
308
-
309
- def handle_schema_error(
310
- error: OperationSchemaError,
311
- results: TestResultSet,
312
- data_generation_methods: Iterable[DataGenerationMethod],
313
- recursion_level: int,
314
- *,
315
- before_execution_correlation_id: str | None = None,
316
- ) -> Generator[events.ExecutionEvent, None, None]:
317
- if error.method is not None:
318
- assert error.path is not None
319
- assert error.full_path is not None
320
- data_generation_methods = list(data_generation_methods)
321
- method = error.method.upper()
322
- verbose_name = f"{method} {error.full_path}"
323
- result = TestResult(
324
- method=method,
325
- path=error.full_path,
326
- verbose_name=verbose_name,
327
- data_generation_method=data_generation_methods,
328
- )
329
- result.add_error(error)
330
- # It might be already emitted - reuse its correlation id
331
- if before_execution_correlation_id is not None:
332
- correlation_id = before_execution_correlation_id
333
- else:
334
- correlation_id = uuid.uuid4().hex
335
- yield events.BeforeExecution(
336
- method=method,
337
- path=error.full_path,
338
- verbose_name=verbose_name,
339
- relative_path=error.path,
340
- recursion_level=recursion_level,
341
- data_generation_method=data_generation_methods,
342
- correlation_id=correlation_id,
343
- )
344
- yield events.AfterExecution(
345
- method=method,
346
- path=error.full_path,
347
- relative_path=error.path,
348
- verbose_name=verbose_name,
349
- status=Status.error,
350
- result=SerializedTestResult.from_test_result(result),
351
- data_generation_method=data_generation_methods,
352
- elapsed_time=0.0,
353
- hypothesis_output=[],
354
- correlation_id=correlation_id,
355
- )
356
- results.append(result)
357
- else:
358
- # When there is no `method`, then the schema error may cover multiple operations, and we can't display it in
359
- # the progress bar
360
- results.generic_errors.append(error)
361
-
362
-
363
- def run_test(
364
- operation: APIOperation,
365
- test: Callable,
366
- checks: Iterable[CheckFunction],
367
- data_generation_methods: Iterable[DataGenerationMethod],
368
- targets: Iterable[Target],
369
- results: TestResultSet,
370
- headers: dict[str, Any] | None,
371
- recursion_level: int,
372
- **kwargs: Any,
373
- ) -> Generator[events.ExecutionEvent, None, None]:
374
- """A single test run with all error handling needed."""
375
- data_generation_methods = list(data_generation_methods)
376
- result = TestResult(
377
- method=operation.method.upper(),
378
- path=operation.full_path,
379
- verbose_name=operation.verbose_name,
380
- data_generation_method=data_generation_methods,
381
- )
382
- # To simplify connecting `before` and `after` events in external systems
383
- correlation_id = uuid.uuid4().hex
384
- yield events.BeforeExecution.from_operation(
385
- operation=operation,
386
- recursion_level=recursion_level,
387
- data_generation_method=data_generation_methods,
388
- correlation_id=correlation_id,
389
- )
390
- hypothesis_output: list[str] = []
391
- errors: list[Exception] = []
392
- test_start_time = time.monotonic()
393
- setup_hypothesis_database_key(test, operation)
394
- try:
395
- with catch_warnings(record=True) as warnings, capture_hypothesis_output() as hypothesis_output:
396
- test(
397
- checks,
398
- targets,
399
- result,
400
- errors=errors,
401
- headers=headers,
402
- data_generation_methods=data_generation_methods,
403
- **kwargs,
404
- )
405
- # Test body was not executed at all - Hypothesis did not generate any tests, but there is no error
406
- if not result.is_executed:
407
- status = Status.skip
408
- result.mark_skipped(None)
409
- else:
410
- status = Status.success
411
- except unittest.case.SkipTest as exc:
412
- # Newer Hypothesis versions raise this exception if no tests were executed
413
- status = Status.skip
414
- result.mark_skipped(exc)
415
- except CheckFailed:
416
- status = Status.failure
417
- except NonCheckError:
418
- # It could be an error in user-defined extensions, network errors or internal Schemathesis errors
419
- status = Status.error
420
- result.mark_errored()
421
- for error in deduplicate_errors(errors):
422
- result.add_error(error)
423
- except MultipleFailures:
424
- # Schemathesis may detect multiple errors that come from different check results
425
- # They raise different "grouped" exceptions
426
- if errors:
427
- status = Status.error
428
- add_errors(result, errors)
429
- else:
430
- status = Status.failure
431
- except hypothesis.errors.Flaky as exc:
432
- if isinstance(exc.__cause__, hypothesis.errors.DeadlineExceeded):
433
- status = Status.error
434
- result.add_error(DeadlineExceeded.from_exc(exc.__cause__))
435
- elif errors:
436
- status = Status.error
437
- add_errors(result, errors)
438
- else:
439
- status = Status.failure
440
- result.mark_flaky()
441
- except hypothesis.errors.Unsatisfiable:
442
- # We need more clear error message here
443
- status = Status.error
444
- result.add_error(hypothesis.errors.Unsatisfiable("Failed to generate test cases for this API operation"))
445
- except KeyboardInterrupt:
446
- yield events.Interrupted()
447
- return
448
- except SkipTest as exc:
449
- status = Status.skip
450
- result.mark_skipped(exc)
451
- except AssertionError: # comes from `hypothesis-jsonschema`
452
- error = reraise(operation)
453
- status = Status.error
454
- result.add_error(error)
455
- except HypothesisRefResolutionError:
456
- status = Status.error
457
- result.add_error(hypothesis.errors.Unsatisfiable(RECURSIVE_REFERENCE_ERROR_MESSAGE))
458
- except InvalidArgument as error:
459
- status = Status.error
460
- message = get_invalid_regular_expression_message(warnings)
461
- if message:
462
- # `hypothesis-jsonschema` emits a warning on invalid regular expression syntax
463
- result.add_error(InvalidRegularExpression.from_hypothesis_jsonschema_message(message))
464
- else:
465
- result.add_error(error)
466
- except hypothesis.errors.DeadlineExceeded as error:
467
- status = Status.error
468
- result.add_error(DeadlineExceeded.from_exc(error))
469
- except JsonSchemaError as error:
470
- status = Status.error
471
- result.add_error(InvalidRegularExpression.from_schema_error(error, from_examples=False))
472
- except Exception as error:
473
- status = Status.error
474
- # Likely a YAML parsing issue. E.g. `00:00:00.00` (without quotes) is parsed as float `0.0`
475
- if str(error) == "first argument must be string or compiled pattern":
476
- result.add_error(
477
- InvalidRegularExpression(
478
- "Invalid `pattern` value: expected a string. "
479
- "If your schema is in YAML, ensure `pattern` values are quoted",
480
- is_valid_type=False,
481
- )
482
- )
483
- else:
484
- result.add_error(error)
485
- if has_unsatisfied_example_mark(test):
486
- status = Status.error
487
- result.add_error(
488
- hypothesis.errors.Unsatisfiable("Failed to generate test cases from examples for this API operation")
489
- )
490
- non_serializable = get_non_serializable_mark(test)
491
- if non_serializable is not None and status != Status.error:
492
- status = Status.error
493
- media_types = ", ".join(non_serializable.media_types)
494
- result.add_error(
495
- SerializationNotPossible(
496
- "Failed to generate test cases from examples for this API operation because of"
497
- f" unsupported payload media types: {media_types}\n{SERIALIZERS_SUGGESTION_MESSAGE}",
498
- media_types=non_serializable.media_types,
499
- )
500
- )
501
- invalid_regex = get_invalid_regex_mark(test)
502
- if invalid_regex is not None and status != Status.error:
503
- status = Status.error
504
- result.add_error(InvalidRegularExpression.from_schema_error(invalid_regex, from_examples=True))
505
- invalid_headers = get_invalid_example_headers_mark(test)
506
- if invalid_headers:
507
- status = Status.error
508
- result.add_error(InvalidHeadersExample.from_headers(invalid_headers))
509
- test_elapsed_time = time.monotonic() - test_start_time
510
- # DEPRECATED: Seed is the same per test run
511
- # Fetch seed value, hypothesis generates it during test execution
512
- # It may be `None` if the `derandomize` config option is set to `True`
513
- result.seed = getattr(test, "_hypothesis_internal_use_seed", None) or getattr(
514
- test, "_hypothesis_internal_use_generated_seed", None
515
- )
516
- results.append(result)
517
- for status_code in (401, 403):
518
- if has_too_many_responses_with_status(result, status_code):
519
- results.add_warning(TOO_MANY_RESPONSES_WARNING_TEMPLATE.format(f"`{operation.verbose_name}`", status_code))
520
- yield events.AfterExecution.from_result(
521
- result=result,
522
- status=status,
523
- elapsed_time=test_elapsed_time,
524
- hypothesis_output=hypothesis_output,
525
- operation=operation,
526
- data_generation_method=data_generation_methods,
527
- correlation_id=correlation_id,
528
- )
529
-
530
-
531
- TOO_MANY_RESPONSES_WARNING_TEMPLATE = (
532
- "Most of the responses from {} have a {} status code. Did you specify proper API credentials?"
533
- )
534
- TOO_MANY_RESPONSES_THRESHOLD = 0.9
535
-
536
-
537
- def has_too_many_responses_with_status(result: TestResult, status_code: int) -> bool:
538
- # It is faster than creating an intermediate list
539
- unauthorized_count = 0
540
- total = 0
541
- for check in result.checks:
542
- if check.response is not None:
543
- if check.response.status_code == status_code:
544
- unauthorized_count += 1
545
- total += 1
546
- if not total:
547
- return False
548
- return unauthorized_count / total >= TOO_MANY_RESPONSES_THRESHOLD
549
-
550
-
551
- ALL_NOT_FOUND_WARNING_MESSAGE = "All API responses have a 404 status code. Did you specify the proper API location?"
552
-
553
-
554
- def has_all_not_found(results: TestResultSet) -> bool:
555
- """Check if all responses are 404."""
556
- has_not_found = False
557
- for result in results.results:
558
- for check in result.checks:
559
- if check.response is not None:
560
- if check.response.status_code == 404:
561
- has_not_found = True
562
- else:
563
- # There are non-404 responses, no reason to check any other response
564
- return False
565
- # Only happens if all responses are 404, or there are no responses at all.
566
- # In the first case, it returns True, for the latter - False
567
- return has_not_found
568
-
569
-
570
- def setup_hypothesis_database_key(test: Callable, operation: APIOperation) -> None:
571
- """Make Hypothesis use separate database entries for every API operation.
572
-
573
- It increases the effectiveness of the Hypothesis database in the CLI.
574
- """
575
- # Hypothesis's function digest depends on the test function signature. To reflect it for the web API case,
576
- # we use all API operation parameters in the digest.
577
- extra = operation.verbose_name.encode("utf8")
578
- for parameter in operation.definition.parameters:
579
- extra += parameter.serialize(operation).encode("utf8")
580
- test.hypothesis.inner_test._hypothesis_internal_add_digest = extra # type: ignore
581
-
582
-
583
- def get_invalid_regular_expression_message(warnings: list[WarningMessage]) -> str | None:
584
- for warning in warnings:
585
- message = str(warning.message)
586
- if "is not valid syntax for a Python regular expression" in message:
587
- return message
588
- return None
589
-
590
-
591
- def reraise(operation: APIOperation) -> OperationSchemaError:
592
- try:
593
- operation.schema.validate()
594
- except ValidationError as exc:
595
- return OperationSchemaError.from_jsonschema_error(
596
- exc, path=operation.path, method=operation.method, full_path=operation.schema.get_full_path(operation.path)
597
- )
598
- return OperationSchemaError("Unknown schema error")
599
-
600
-
601
- MEMORY_ADDRESS_RE = re.compile("0x[0-9a-fA-F]+")
602
- URL_IN_ERROR_MESSAGE_RE = re.compile(r"Max retries exceeded with url: .*? \(Caused by")
603
-
604
-
605
- def add_errors(result: TestResult, errors: list[Exception]) -> None:
606
- group_errors(errors)
607
- for error in deduplicate_errors(errors):
608
- result.add_error(error)
609
-
610
-
611
- def group_errors(errors: list[Exception]) -> None:
612
- """Group errors of the same kind info a single one, avoiding duplicate error messages."""
613
- serialization_errors = [error for error in errors if isinstance(error, SerializationNotPossible)]
614
- if len(serialization_errors) > 1:
615
- errors[:] = [error for error in errors if not isinstance(error, SerializationNotPossible)]
616
- media_types = sum((entry.media_types for entry in serialization_errors), [])
617
- errors.append(SerializationNotPossible.from_media_types(*media_types))
618
-
619
-
620
- def canonicalize_error_message(error: Exception, include_traceback: bool = True) -> str:
621
- message = format_exception(error, include_traceback)
622
- # Replace memory addresses with a fixed string
623
- message = MEMORY_ADDRESS_RE.sub("0xbaaaaaaaaaad", message)
624
- return URL_IN_ERROR_MESSAGE_RE.sub("", message)
625
-
626
-
627
- def deduplicate_errors(errors: list[Exception]) -> Generator[Exception, None, None]:
628
- """Deduplicate errors by their messages + tracebacks."""
629
- seen = set()
630
- for error in errors:
631
- message = canonicalize_error_message(error)
632
- if message in seen:
633
- continue
634
- seen.add(message)
635
- yield error
636
-
637
-
638
- def run_checks(
639
- *,
640
- case: Case,
641
- checks: Iterable[CheckFunction],
642
- check_results: list[Check],
643
- result: TestResult,
644
- response: GenericResponse,
645
- elapsed_time: float,
646
- max_response_time: int | None = None,
647
- ) -> None:
648
- errors = []
649
-
650
- def add_single_failure(error: AssertionError) -> None:
651
- msg = maybe_set_assertion_message(error, check_name)
652
- errors.append(error)
653
- if isinstance(error, CheckFailed):
654
- context = error.context
655
- else:
656
- context = None
657
- check_results.append(result.add_failure(check_name, copied_case, response, elapsed_time, msg, context))
658
-
659
- for check in checks:
660
- check_name = check.__name__
661
- copied_case = case.partial_deepcopy()
662
- try:
663
- skip_check = check(response, copied_case)
664
- if not skip_check:
665
- check_result = result.add_success(check_name, copied_case, response, elapsed_time)
666
- check_results.append(check_result)
667
- except AssertionError as exc:
668
- add_single_failure(exc)
669
- except MultipleFailures as exc:
670
- for exception in exc.exceptions:
671
- add_single_failure(exception)
672
-
673
- if max_response_time:
674
- if elapsed_time > max_response_time:
675
- message = f"Actual: {elapsed_time:.2f}ms\nLimit: {max_response_time}.00ms"
676
- errors.append(AssertionError(message))
677
- result.add_failure(
678
- "max_response_time",
679
- case,
680
- response,
681
- elapsed_time,
682
- message,
683
- failures.ResponseTimeExceeded(message=message, elapsed=elapsed_time, deadline=max_response_time),
684
- )
685
- else:
686
- result.add_success("max_response_time", case, response, elapsed_time)
687
-
688
- if errors:
689
- raise get_grouped_exception(case.operation.verbose_name, *errors)(causes=tuple(errors))
690
-
691
-
692
- def run_targets(targets: Iterable[Callable], context: TargetContext) -> None:
693
- for target in targets:
694
- value = target(context)
695
- hypothesis.target(value, label=target.__name__)
696
-
697
-
698
- def add_cases(case: Case, response: GenericResponse, test: Callable, *args: Any) -> None:
699
- context = HookContext(case.operation)
700
- for case_hook in get_all_by_name("add_case"):
701
- _case = case_hook(context, case.partial_deepcopy(), response)
702
- # run additional test if _case is not an empty value
703
- if _case:
704
- test(_case, *args)
705
-
706
-
707
- @dataclass
708
- class ErrorCollector:
709
- """Collect exceptions that are not related to failed checks.
710
-
711
- Such exceptions may be considered as multiple failures or flakiness by Hypothesis. In both cases, Hypothesis hides
712
- exception information that, in our case, is helpful for the end-user. It either indicates errors in user-defined
713
- extensions, network-related errors, or internal Schemathesis errors. In all cases, this information is useful for
714
- debugging.
715
-
716
- To mitigate this, we gather all exceptions manually via this context manager to avoid interfering with the test
717
- function signatures, which are used by Hypothesis.
718
- """
719
-
720
- errors: list[Exception]
721
-
722
- def __enter__(self) -> ErrorCollector:
723
- return self
724
-
725
- def __exit__(
726
- self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None
727
- ) -> Literal[False]:
728
- # Don't do anything special if:
729
- # - Tests are successful
730
- # - Checks failed
731
- # - The testing process is interrupted
732
- if not exc_type or issubclass(exc_type, CheckFailed) or not issubclass(exc_type, Exception):
733
- return False
734
- # These exceptions are needed for control flow on the Hypothesis side. E.g. rejecting unsatisfiable examples
735
- if isinstance(exc_val, HypothesisException):
736
- raise
737
- # Exception value is not `None` and is a subclass of `Exception` at this point
738
- exc_val = cast(Exception, exc_val)
739
- self.errors.append(exc_val.with_traceback(exc_tb))
740
- raise NonCheckError from None
741
-
742
-
743
- def _force_data_generation_method(values: list[DataGenerationMethod], case: Case) -> None:
744
- # Set data generation method to the one that actually used
745
- data_generation_method = cast(DataGenerationMethod, case.data_generation_method)
746
- values[:] = [data_generation_method]
747
-
748
-
749
- def network_test(
750
- case: Case,
751
- checks: Iterable[CheckFunction],
752
- targets: Iterable[Target],
753
- result: TestResult,
754
- session: requests.Session,
755
- request_timeout: int | None,
756
- request_tls_verify: bool,
757
- request_proxy: str | None,
758
- request_cert: RequestCert | None,
759
- store_interactions: bool,
760
- headers: dict[str, Any] | None,
761
- feedback: Feedback,
762
- max_response_time: int | None,
763
- data_generation_methods: list[DataGenerationMethod],
764
- dry_run: bool,
765
- errors: list[Exception],
766
- ) -> None:
767
- """A single test body will be executed against the target."""
768
- with ErrorCollector(errors):
769
- _force_data_generation_method(data_generation_methods, case)
770
- result.mark_executed()
771
- headers = headers or {}
772
- if "user-agent" not in {header.lower() for header in headers}:
773
- headers["User-Agent"] = USER_AGENT
774
- timeout = prepare_timeout(request_timeout)
775
- if not dry_run:
776
- args = (
777
- checks,
778
- targets,
779
- result,
780
- session,
781
- timeout,
782
- store_interactions,
783
- headers,
784
- feedback,
785
- request_tls_verify,
786
- request_proxy,
787
- request_cert,
788
- max_response_time,
789
- )
790
- response = _network_test(case, *args)
791
- add_cases(case, response, _network_test, *args)
792
-
793
-
794
- def _network_test(
795
- case: Case,
796
- checks: Iterable[CheckFunction],
797
- targets: Iterable[Target],
798
- result: TestResult,
799
- session: requests.Session,
800
- timeout: float | None,
801
- store_interactions: bool,
802
- headers: dict[str, Any] | None,
803
- feedback: Feedback,
804
- request_tls_verify: bool,
805
- request_proxy: str | None,
806
- request_cert: RequestCert | None,
807
- max_response_time: int | None,
808
- ) -> requests.Response:
809
- check_results: list[Check] = []
810
- try:
811
- hook_context = HookContext(operation=case.operation)
812
- kwargs: dict[str, Any] = {
813
- "session": session,
814
- "headers": headers,
815
- "timeout": timeout,
816
- "verify": request_tls_verify,
817
- "cert": request_cert,
818
- }
819
- if request_proxy is not None:
820
- kwargs["proxies"] = {"all": request_proxy}
821
- hooks.dispatch("process_call_kwargs", hook_context, case, kwargs)
822
- response = case.call(**kwargs)
823
- except CheckFailed as exc:
824
- check_name = "request_timeout"
825
- requests_kwargs = case.as_requests_kwargs(base_url=case.get_full_base_url(), headers=headers)
826
- request = requests.Request(**requests_kwargs).prepare()
827
- elapsed = cast(float, timeout) # It is defined and not empty, since the exception happened
828
- check_result = result.add_failure(
829
- check_name, case, None, elapsed, f"Response timed out after {1000 * elapsed:.2f}ms", exc.context, request
830
- )
831
- check_results.append(check_result)
832
- raise exc
833
- context = TargetContext(case=case, response=response, response_time=response.elapsed.total_seconds())
834
- run_targets(targets, context)
835
- status = Status.success
836
- try:
837
- run_checks(
838
- case=case,
839
- checks=checks,
840
- check_results=check_results,
841
- result=result,
842
- response=response,
843
- elapsed_time=context.response_time * 1000,
844
- max_response_time=max_response_time,
845
- )
846
- except CheckFailed:
847
- status = Status.failure
848
- raise
849
- finally:
850
- feedback.add_test_case(case, response)
851
- if store_interactions:
852
- result.store_requests_response(case, response, status, check_results)
853
- return response
854
-
855
-
856
- @contextmanager
857
- def get_session(auth: HTTPDigestAuth | RawAuth | None = None) -> Generator[requests.Session, None, None]:
858
- with requests.Session() as session:
859
- if auth is not None:
860
- session.auth = auth
861
- yield session
862
-
863
-
864
- def prepare_timeout(timeout: int | None) -> float | None:
865
- """Request timeout is in milliseconds, but `requests` uses seconds."""
866
- output: int | float | None = timeout
867
- if timeout is not None:
868
- output = timeout / 1000
869
- return output
870
-
871
-
872
- def wsgi_test(
873
- case: Case,
874
- checks: Iterable[CheckFunction],
875
- targets: Iterable[Target],
876
- result: TestResult,
877
- auth: RawAuth | None,
878
- auth_type: str | None,
879
- headers: dict[str, Any] | None,
880
- store_interactions: bool,
881
- feedback: Feedback,
882
- max_response_time: int | None,
883
- data_generation_methods: list[DataGenerationMethod],
884
- dry_run: bool,
885
- errors: list[Exception],
886
- ) -> None:
887
- with ErrorCollector(errors):
888
- _force_data_generation_method(data_generation_methods, case)
889
- result.mark_executed()
890
- headers = _prepare_wsgi_headers(headers, auth, auth_type)
891
- if not dry_run:
892
- args = (
893
- checks,
894
- targets,
895
- result,
896
- headers,
897
- store_interactions,
898
- feedback,
899
- max_response_time,
900
- )
901
- response = _wsgi_test(case, *args)
902
- add_cases(case, response, _wsgi_test, *args)
903
-
904
-
905
- def _wsgi_test(
906
- case: Case,
907
- checks: Iterable[CheckFunction],
908
- targets: Iterable[Target],
909
- result: TestResult,
910
- headers: dict[str, Any],
911
- store_interactions: bool,
912
- feedback: Feedback,
913
- max_response_time: int | None,
914
- ) -> WSGIResponse:
915
- with catching_logs(LogCaptureHandler(), level=logging.DEBUG) as recorded:
916
- start = time.monotonic()
917
- hook_context = HookContext(operation=case.operation)
918
- kwargs = {"headers": headers}
919
- hooks.dispatch("process_call_kwargs", hook_context, case, kwargs)
920
- response = case.call_wsgi(**kwargs)
921
- elapsed = time.monotonic() - start
922
- context = TargetContext(case=case, response=response, response_time=elapsed)
923
- run_targets(targets, context)
924
- result.logs.extend(recorded.records)
925
- status = Status.success
926
- check_results: list[Check] = []
927
- try:
928
- run_checks(
929
- case=case,
930
- checks=checks,
931
- check_results=check_results,
932
- result=result,
933
- response=response,
934
- elapsed_time=context.response_time * 1000,
935
- max_response_time=max_response_time,
936
- )
937
- except CheckFailed:
938
- status = Status.failure
939
- raise
940
- finally:
941
- feedback.add_test_case(case, response)
942
- if store_interactions:
943
- result.store_wsgi_response(case, response, headers, elapsed, status, check_results)
944
- return response
945
-
946
-
947
- def _prepare_wsgi_headers(
948
- headers: dict[str, Any] | None, auth: RawAuth | None, auth_type: str | None
949
- ) -> dict[str, Any]:
950
- headers = headers or {}
951
- if "user-agent" not in {header.lower() for header in headers}:
952
- headers["User-Agent"] = USER_AGENT
953
- wsgi_auth = get_wsgi_auth(auth, auth_type)
954
- if wsgi_auth:
955
- headers["Authorization"] = wsgi_auth
956
- return headers
957
-
958
-
959
- def get_wsgi_auth(auth: RawAuth | None, auth_type: str | None) -> str | None:
960
- if auth:
961
- if auth_type == "digest":
962
- raise ValueError("Digest auth is not supported for WSGI apps")
963
- return _basic_auth_str(*auth)
964
- return None
965
-
966
-
967
- def asgi_test(
968
- case: Case,
969
- checks: Iterable[CheckFunction],
970
- targets: Iterable[Target],
971
- result: TestResult,
972
- store_interactions: bool,
973
- headers: dict[str, Any] | None,
974
- feedback: Feedback,
975
- max_response_time: int | None,
976
- data_generation_methods: list[DataGenerationMethod],
977
- dry_run: bool,
978
- errors: list[Exception],
979
- ) -> None:
980
- """A single test body will be executed against the target."""
981
- with ErrorCollector(errors):
982
- _force_data_generation_method(data_generation_methods, case)
983
- result.mark_executed()
984
- headers = headers or {}
985
-
986
- if not dry_run:
987
- args = (
988
- checks,
989
- targets,
990
- result,
991
- store_interactions,
992
- headers,
993
- feedback,
994
- max_response_time,
995
- )
996
- response = _asgi_test(case, *args)
997
- add_cases(case, response, _asgi_test, *args)
998
-
999
-
1000
- def _asgi_test(
1001
- case: Case,
1002
- checks: Iterable[CheckFunction],
1003
- targets: Iterable[Target],
1004
- result: TestResult,
1005
- store_interactions: bool,
1006
- headers: dict[str, Any] | None,
1007
- feedback: Feedback,
1008
- max_response_time: int | None,
1009
- ) -> requests.Response:
1010
- hook_context = HookContext(operation=case.operation)
1011
- kwargs: dict[str, Any] = {"headers": headers}
1012
- hooks.dispatch("process_call_kwargs", hook_context, case, kwargs)
1013
- response = case.call_asgi(**kwargs)
1014
- context = TargetContext(case=case, response=response, response_time=response.elapsed.total_seconds())
1015
- run_targets(targets, context)
1016
- status = Status.success
1017
- check_results: list[Check] = []
1018
- try:
1019
- run_checks(
1020
- case=case,
1021
- checks=checks,
1022
- check_results=check_results,
1023
- result=result,
1024
- response=response,
1025
- elapsed_time=context.response_time * 1000,
1026
- max_response_time=max_response_time,
1027
- )
1028
- except CheckFailed:
1029
- status = Status.failure
1030
- raise
1031
- finally:
1032
- feedback.add_test_case(case, response)
1033
- if store_interactions:
1034
- result.store_requests_response(case, response, status, check_results)
1035
- return response