schemathesis 3.25.6__py3-none-any.whl → 3.39.7__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 (146) hide show
  1. schemathesis/__init__.py +6 -6
  2. schemathesis/_compat.py +2 -2
  3. schemathesis/_dependency_versions.py +4 -2
  4. schemathesis/_hypothesis.py +369 -56
  5. schemathesis/_lazy_import.py +1 -0
  6. schemathesis/_override.py +5 -4
  7. schemathesis/_patches.py +21 -0
  8. schemathesis/_rate_limiter.py +7 -0
  9. schemathesis/_xml.py +75 -22
  10. schemathesis/auths.py +78 -16
  11. schemathesis/checks.py +21 -9
  12. schemathesis/cli/__init__.py +783 -432
  13. schemathesis/cli/__main__.py +4 -0
  14. schemathesis/cli/callbacks.py +58 -13
  15. schemathesis/cli/cassettes.py +233 -47
  16. schemathesis/cli/constants.py +8 -2
  17. schemathesis/cli/context.py +22 -5
  18. schemathesis/cli/debug.py +2 -1
  19. schemathesis/cli/handlers.py +4 -1
  20. schemathesis/cli/junitxml.py +103 -22
  21. schemathesis/cli/options.py +15 -4
  22. schemathesis/cli/output/default.py +258 -112
  23. schemathesis/cli/output/short.py +23 -8
  24. schemathesis/cli/reporting.py +79 -0
  25. schemathesis/cli/sanitization.py +6 -0
  26. schemathesis/code_samples.py +5 -3
  27. schemathesis/constants.py +1 -0
  28. schemathesis/contrib/openapi/__init__.py +1 -1
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
  30. schemathesis/contrib/openapi/formats/uuid.py +2 -1
  31. schemathesis/contrib/unique_data.py +3 -3
  32. schemathesis/exceptions.py +76 -65
  33. schemathesis/experimental/__init__.py +35 -0
  34. schemathesis/extra/_aiohttp.py +1 -0
  35. schemathesis/extra/_flask.py +4 -1
  36. schemathesis/extra/_server.py +1 -0
  37. schemathesis/extra/pytest_plugin.py +17 -25
  38. schemathesis/failures.py +77 -9
  39. schemathesis/filters.py +185 -8
  40. schemathesis/fixups/__init__.py +1 -0
  41. schemathesis/fixups/fast_api.py +2 -2
  42. schemathesis/fixups/utf8_bom.py +1 -2
  43. schemathesis/generation/__init__.py +20 -36
  44. schemathesis/generation/_hypothesis.py +59 -0
  45. schemathesis/generation/_methods.py +44 -0
  46. schemathesis/generation/coverage.py +931 -0
  47. schemathesis/graphql.py +0 -1
  48. schemathesis/hooks.py +89 -12
  49. schemathesis/internal/checks.py +84 -0
  50. schemathesis/internal/copy.py +22 -3
  51. schemathesis/internal/deprecation.py +6 -2
  52. schemathesis/internal/diff.py +15 -0
  53. schemathesis/internal/extensions.py +27 -0
  54. schemathesis/internal/jsonschema.py +2 -1
  55. schemathesis/internal/output.py +68 -0
  56. schemathesis/internal/result.py +1 -1
  57. schemathesis/internal/transformation.py +11 -0
  58. schemathesis/lazy.py +138 -25
  59. schemathesis/loaders.py +7 -5
  60. schemathesis/models.py +318 -211
  61. schemathesis/parameters.py +4 -0
  62. schemathesis/runner/__init__.py +50 -15
  63. schemathesis/runner/events.py +65 -5
  64. schemathesis/runner/impl/context.py +104 -0
  65. schemathesis/runner/impl/core.py +388 -177
  66. schemathesis/runner/impl/solo.py +19 -29
  67. schemathesis/runner/impl/threadpool.py +70 -79
  68. schemathesis/runner/probes.py +11 -9
  69. schemathesis/runner/serialization.py +150 -17
  70. schemathesis/sanitization.py +5 -1
  71. schemathesis/schemas.py +170 -102
  72. schemathesis/serializers.py +7 -2
  73. schemathesis/service/ci.py +1 -0
  74. schemathesis/service/client.py +39 -6
  75. schemathesis/service/events.py +5 -1
  76. schemathesis/service/extensions.py +224 -0
  77. schemathesis/service/hosts.py +6 -2
  78. schemathesis/service/metadata.py +25 -0
  79. schemathesis/service/models.py +211 -2
  80. schemathesis/service/report.py +6 -6
  81. schemathesis/service/serialization.py +45 -71
  82. schemathesis/service/usage.py +1 -0
  83. schemathesis/specs/graphql/_cache.py +26 -0
  84. schemathesis/specs/graphql/loaders.py +25 -5
  85. schemathesis/specs/graphql/nodes.py +1 -0
  86. schemathesis/specs/graphql/scalars.py +2 -2
  87. schemathesis/specs/graphql/schemas.py +130 -100
  88. schemathesis/specs/graphql/validation.py +1 -2
  89. schemathesis/specs/openapi/__init__.py +1 -0
  90. schemathesis/specs/openapi/_cache.py +123 -0
  91. schemathesis/specs/openapi/_hypothesis.py +78 -60
  92. schemathesis/specs/openapi/checks.py +504 -25
  93. schemathesis/specs/openapi/converter.py +31 -4
  94. schemathesis/specs/openapi/definitions.py +10 -17
  95. schemathesis/specs/openapi/examples.py +126 -12
  96. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  97. schemathesis/specs/openapi/expressions/context.py +1 -1
  98. schemathesis/specs/openapi/expressions/extractors.py +26 -0
  99. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  100. schemathesis/specs/openapi/expressions/nodes.py +29 -6
  101. schemathesis/specs/openapi/expressions/parser.py +26 -5
  102. schemathesis/specs/openapi/formats.py +44 -0
  103. schemathesis/specs/openapi/links.py +125 -42
  104. schemathesis/specs/openapi/loaders.py +77 -36
  105. schemathesis/specs/openapi/media_types.py +34 -0
  106. schemathesis/specs/openapi/negative/__init__.py +6 -3
  107. schemathesis/specs/openapi/negative/mutations.py +21 -6
  108. schemathesis/specs/openapi/parameters.py +39 -25
  109. schemathesis/specs/openapi/patterns.py +137 -0
  110. schemathesis/specs/openapi/references.py +37 -7
  111. schemathesis/specs/openapi/schemas.py +360 -241
  112. schemathesis/specs/openapi/security.py +25 -7
  113. schemathesis/specs/openapi/serialization.py +1 -0
  114. schemathesis/specs/openapi/stateful/__init__.py +198 -70
  115. schemathesis/specs/openapi/stateful/statistic.py +198 -0
  116. schemathesis/specs/openapi/stateful/types.py +14 -0
  117. schemathesis/specs/openapi/utils.py +6 -1
  118. schemathesis/specs/openapi/validation.py +1 -0
  119. schemathesis/stateful/__init__.py +35 -21
  120. schemathesis/stateful/config.py +97 -0
  121. schemathesis/stateful/context.py +135 -0
  122. schemathesis/stateful/events.py +274 -0
  123. schemathesis/stateful/runner.py +309 -0
  124. schemathesis/stateful/sink.py +68 -0
  125. schemathesis/stateful/state_machine.py +67 -38
  126. schemathesis/stateful/statistic.py +22 -0
  127. schemathesis/stateful/validation.py +100 -0
  128. schemathesis/targets.py +33 -1
  129. schemathesis/throttling.py +25 -5
  130. schemathesis/transports/__init__.py +354 -0
  131. schemathesis/transports/asgi.py +7 -0
  132. schemathesis/transports/auth.py +25 -2
  133. schemathesis/transports/content_types.py +3 -1
  134. schemathesis/transports/headers.py +2 -1
  135. schemathesis/transports/responses.py +9 -4
  136. schemathesis/types.py +9 -0
  137. schemathesis/utils.py +11 -16
  138. schemathesis-3.39.7.dist-info/METADATA +293 -0
  139. schemathesis-3.39.7.dist-info/RECORD +160 -0
  140. {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
  141. schemathesis/specs/openapi/filters.py +0 -49
  142. schemathesis/specs/openapi/stateful/links.py +0 -92
  143. schemathesis-3.25.6.dist-info/METADATA +0 -356
  144. schemathesis-3.25.6.dist-info/RECORD +0 -134
  145. {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
  146. {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
@@ -1,14 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import functools
3
4
  import logging
5
+ import operator
4
6
  import re
5
7
  import threading
6
8
  import time
7
9
  import unittest
8
10
  import uuid
11
+ import warnings
9
12
  from contextlib import contextmanager
10
13
  from dataclasses import dataclass, field
11
- from types import TracebackType
12
14
  from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, List, Literal, cast
13
15
  from warnings import WarningMessage, catch_warnings
14
16
 
@@ -19,9 +21,10 @@ from hypothesis.errors import HypothesisException, InvalidArgument
19
21
  from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
20
22
  from jsonschema.exceptions import SchemaError as JsonSchemaError
21
23
  from jsonschema.exceptions import ValidationError
22
- from requests.auth import HTTPDigestAuth, _basic_auth_str
24
+ from requests.structures import CaseInsensitiveDict
25
+ from urllib3.exceptions import InsecureRequestWarning
23
26
 
24
- from ... import failures, hooks
27
+ from ... import experimental, failures, hooks
25
28
  from ..._compat import MultipleFailures
26
29
  from ..._hypothesis import (
27
30
  get_invalid_example_headers_mark,
@@ -29,8 +32,8 @@ from ..._hypothesis import (
29
32
  get_non_serializable_mark,
30
33
  has_unsatisfied_example_mark,
31
34
  )
32
- from ..._override import CaseOverride
33
35
  from ...auths import unregister as unregister_auth
36
+ from ...checks import _make_max_response_time_failure_message
34
37
  from ...constants import (
35
38
  DEFAULT_STATEFUL_RECURSION_LIMIT,
36
39
  RECURSIVE_REFERENCE_ERROR_MESSAGE,
@@ -40,10 +43,12 @@ from ...constants import (
40
43
  from ...exceptions import (
41
44
  CheckFailed,
42
45
  DeadlineExceeded,
46
+ InternalError,
43
47
  InvalidHeadersExample,
44
48
  InvalidRegularExpression,
45
49
  NonCheckError,
46
50
  OperationSchemaError,
51
+ RecursiveReferenceError,
47
52
  SerializationNotPossible,
48
53
  SkipTest,
49
54
  format_exception,
@@ -52,21 +57,36 @@ from ...exceptions import (
52
57
  )
53
58
  from ...generation import DataGenerationMethod, GenerationConfig
54
59
  from ...hooks import HookContext, get_all_by_name
60
+ from ...internal.checks import CheckConfig, CheckContext
55
61
  from ...internal.datetime import current_datetime
56
- from ...internal.result import Ok
57
- from ...models import APIOperation, Case, Check, CheckFunction, Status, TestResult, TestResultSet
62
+ from ...internal.result import Err, Ok, Result
63
+ from ...models import APIOperation, Case, Check, Status, TestResult
58
64
  from ...runner import events
59
- from ...schemas import BaseSchema
65
+ from ...service import extensions
66
+ from ...service.models import AnalysisResult, AnalysisSuccess
60
67
  from ...specs.openapi import formats
61
68
  from ...stateful import Feedback, Stateful
69
+ from ...stateful import events as stateful_events
70
+ from ...stateful import runner as stateful_runner
62
71
  from ...targets import Target, TargetContext
63
- from ...types import RawAuth, RequestCert
72
+ from ...transports import RequestConfig, RequestsTransport
73
+ from ...transports.auth import get_requests_auth, prepare_wsgi_headers
64
74
  from ...utils import capture_hypothesis_output
65
75
  from .. import probes
66
76
  from ..serialization import SerializedTestResult
77
+ from .context import RunnerContext
67
78
 
68
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
69
88
  from ...transports.responses import GenericResponse, WSGIResponse
89
+ from ...types import RawAuth
70
90
 
71
91
 
72
92
  def _should_count_towards_stop(event: events.ExecutionEvent) -> bool:
@@ -80,24 +100,29 @@ class BaseRunner:
80
100
  max_response_time: int | None
81
101
  targets: Iterable[Target]
82
102
  hypothesis_settings: hypothesis.settings
83
- generation_config: GenerationConfig
103
+ generation_config: GenerationConfig | None
84
104
  probe_config: probes.ProbeConfig
105
+ checks_config: CheckConfig
106
+ request_config: RequestConfig = field(default_factory=RequestConfig)
85
107
  override: CaseOverride | None = None
86
108
  auth: RawAuth | None = None
87
109
  auth_type: str | None = None
88
110
  headers: dict[str, Any] | None = None
89
- request_timeout: int | None = None
90
111
  store_interactions: bool = False
91
112
  seed: int | None = None
92
113
  exit_first: bool = False
114
+ no_failfast: bool = False
93
115
  max_failures: int | None = None
94
116
  started_at: str = field(default_factory=current_datetime)
117
+ unique_data: bool = False
95
118
  dry_run: bool = False
96
119
  stateful: Stateful | None = None
97
120
  stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT
98
121
  count_operations: bool = True
99
122
  count_links: bool = True
123
+ service_client: ServiceClient | None = None
100
124
  _failures_counter: int = 0
125
+ _is_stopping_due_to_failure_limit: bool = False
101
126
 
102
127
  def execute(self) -> EventStream:
103
128
  """Common logic for all runners."""
@@ -108,10 +133,19 @@ class BaseRunner:
108
133
  # If auth is explicitly provided, then the global provider is ignored
109
134
  if self.auth is not None:
110
135
  unregister_auth()
111
- results = TestResultSet(seed=self.seed)
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()
112
146
  initialized = None
113
147
  __probes = None
114
- start_time = time.monotonic()
148
+ __analysis: Result[AnalysisResult, Exception] | None = None
115
149
 
116
150
  def _initialize() -> events.Initialized:
117
151
  nonlocal initialized
@@ -119,15 +153,15 @@ class BaseRunner:
119
153
  schema=self.schema,
120
154
  count_operations=self.count_operations,
121
155
  count_links=self.count_links,
122
- seed=self.seed,
156
+ seed=ctx.seed,
123
157
  start_time=start_time,
124
158
  )
125
159
  return initialized
126
160
 
127
161
  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)
162
+ if ctx.has_all_not_found:
163
+ ctx.add_warning(ALL_NOT_FOUND_WARNING_MESSAGE)
164
+ return events.Finished.from_results(results=ctx.data, running_time=time.monotonic() - start_time)
131
165
 
132
166
  def _before_probes() -> events.BeforeProbing:
133
167
  return events.BeforeProbing()
@@ -142,7 +176,26 @@ class BaseRunner:
142
176
  _probes = cast(List[probes.ProbeRun], __probes)
143
177
  return events.AfterProbing(probes=_probes)
144
178
 
145
- if stop_event.is_set():
179
+ def _before_analysis() -> events.BeforeAnalysis:
180
+ return events.BeforeAnalysis()
181
+
182
+ def _run_analysis() -> None:
183
+ nonlocal __analysis, __probes
184
+
185
+ if self.service_client is not None:
186
+ try:
187
+ _probes = cast(List[probes.ProbeRun], __probes)
188
+ result = self.service_client.analyze_schema(_probes, self.schema.raw_schema)
189
+ if isinstance(result, AnalysisSuccess):
190
+ extensions.apply(result.extensions, self.schema)
191
+ __analysis = Ok(result)
192
+ except Exception as exc:
193
+ __analysis = Err(exc)
194
+
195
+ def _after_analysis() -> events.AfterAnalysis:
196
+ return events.AfterAnalysis(analysis=__analysis)
197
+
198
+ if ctx.is_stopped:
146
199
  yield _finish()
147
200
  return
148
201
 
@@ -151,22 +204,35 @@ class BaseRunner:
151
204
  _before_probes,
152
205
  _run_probes,
153
206
  _after_probes,
207
+ _before_analysis,
208
+ _run_analysis,
209
+ _after_analysis,
154
210
  ):
155
211
  event = event_factory()
156
212
  if event is not None:
157
213
  yield event
158
- if stop_event.is_set():
159
- yield _finish()
214
+ if ctx.is_stopped:
215
+ yield _finish() # type: ignore[unreachable]
160
216
  return
161
217
 
162
218
  try:
163
- yield from self._execute(results, stop_event)
219
+ warnings.simplefilter("ignore", InsecureRequestWarning)
220
+ if not experimental.STATEFUL_ONLY.is_enabled:
221
+ yield from self._execute(ctx)
222
+ if not self._is_stopping_due_to_failure_limit:
223
+ yield from self._run_stateful_tests(ctx)
164
224
  except KeyboardInterrupt:
165
225
  yield events.Interrupted()
166
226
 
167
227
  yield _finish()
168
228
 
169
229
  def _should_stop(self, event: events.ExecutionEvent) -> bool:
230
+ result = self.__should_stop(event)
231
+ if result:
232
+ self._is_stopping_due_to_failure_limit = True
233
+ return result
234
+
235
+ def __should_stop(self, event: events.ExecutionEvent) -> bool:
170
236
  if _should_count_towards_stop(event):
171
237
  if self.exit_first:
172
238
  return True
@@ -175,19 +241,116 @@ class BaseRunner:
175
241
  return self._failures_counter >= self.max_failures
176
242
  return False
177
243
 
178
- def _execute(
179
- self, results: TestResultSet, stop_event: threading.Event
180
- ) -> Generator[events.ExecutionEvent, None, None]:
244
+ def _execute(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
181
245
  raise NotImplementedError
182
246
 
247
+ def _run_stateful_tests(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
248
+ # Run new-style stateful tests
249
+ if self.stateful is not None and experimental.STATEFUL_TEST_RUNNER.is_enabled and self.schema.links_count > 0:
250
+ result = TestResult(
251
+ method="",
252
+ path="",
253
+ verbose_name="Stateful tests",
254
+ seed=ctx.seed,
255
+ data_generation_method=self.schema.data_generation_methods,
256
+ )
257
+ headers = self.headers or {}
258
+ if isinstance(self.schema.transport, RequestsTransport):
259
+ auth = get_requests_auth(self.auth, self.auth_type)
260
+ else:
261
+ auth = None
262
+ headers = prepare_wsgi_headers(headers, self.auth, self.auth_type)
263
+ config = stateful_runner.StatefulTestRunnerConfig(
264
+ checks=tuple(self.checks),
265
+ headers=headers,
266
+ hypothesis_settings=self.hypothesis_settings,
267
+ exit_first=self.exit_first,
268
+ max_failures=None if self.max_failures is None else self.max_failures - self._failures_counter,
269
+ request=self.request_config,
270
+ auth=auth,
271
+ seed=ctx.seed,
272
+ override=self.override,
273
+ )
274
+ state_machine = self.schema.as_state_machine()
275
+ runner = state_machine.runner(config=config)
276
+ status = Status.success
277
+
278
+ def from_step_status(step_status: stateful_events.StepStatus) -> Status:
279
+ return {
280
+ stateful_events.StepStatus.SUCCESS: Status.success,
281
+ stateful_events.StepStatus.FAILURE: Status.failure,
282
+ stateful_events.StepStatus.ERROR: Status.error,
283
+ stateful_events.StepStatus.INTERRUPTED: Status.error,
284
+ }[step_status]
285
+
286
+ if self.store_interactions:
287
+ if isinstance(state_machine.schema.transport, RequestsTransport):
288
+
289
+ def on_step_finished(event: stateful_events.StepFinished) -> None:
290
+ if event.response is not None and event.status is not None:
291
+ response = cast(requests.Response, event.response)
292
+ result.store_requests_response(
293
+ status=from_step_status(event.status),
294
+ case=event.case,
295
+ response=response,
296
+ checks=event.checks,
297
+ headers=headers,
298
+ session=None,
299
+ )
300
+
301
+ else:
302
+
303
+ def on_step_finished(event: stateful_events.StepFinished) -> None:
304
+ from ...transports.responses import WSGIResponse
305
+
306
+ if event.response is not None and event.status is not None:
307
+ response = cast(WSGIResponse, event.response)
308
+ result.store_wsgi_response(
309
+ status=from_step_status(event.status),
310
+ case=event.case,
311
+ response=response,
312
+ headers=headers,
313
+ elapsed=response.elapsed.total_seconds(),
314
+ checks=event.checks,
315
+ )
316
+ else:
317
+
318
+ def on_step_finished(event: stateful_events.StepFinished) -> None:
319
+ return None
320
+
321
+ test_start_time: float | None = None
322
+ test_elapsed_time: float | None = None
323
+
324
+ for stateful_event in runner.execute():
325
+ if isinstance(stateful_event, stateful_events.SuiteFinished):
326
+ if stateful_event.failures and status != Status.error:
327
+ status = Status.failure
328
+ elif isinstance(stateful_event, stateful_events.RunStarted):
329
+ test_start_time = stateful_event.timestamp
330
+ elif isinstance(stateful_event, stateful_events.RunFinished):
331
+ test_elapsed_time = stateful_event.timestamp - cast(float, test_start_time)
332
+ elif isinstance(stateful_event, stateful_events.StepFinished):
333
+ result.checks.extend(stateful_event.checks)
334
+ on_step_finished(stateful_event)
335
+ elif isinstance(stateful_event, stateful_events.Errored):
336
+ status = Status.error
337
+ result.add_error(stateful_event.exception)
338
+ yield events.StatefulEvent(data=stateful_event)
339
+ ctx.add_result(result)
340
+ yield events.AfterStatefulExecution(
341
+ status=status,
342
+ result=SerializedTestResult.from_test_result(result),
343
+ elapsed_time=cast(float, test_elapsed_time),
344
+ data_generation_method=self.schema.data_generation_methods,
345
+ )
346
+
183
347
  def _run_tests(
184
348
  self,
185
349
  maker: Callable,
186
- template: Callable,
350
+ test_func: Callable,
187
351
  settings: hypothesis.settings,
188
- generation_config: GenerationConfig,
189
- seed: int | None,
190
- results: TestResultSet,
352
+ generation_config: GenerationConfig | None,
353
+ ctx: RunnerContext,
191
354
  recursion_level: int = 0,
192
355
  headers: dict[str, Any] | None = None,
193
356
  **kwargs: Any,
@@ -207,15 +370,18 @@ class BaseRunner:
207
370
  return kw
208
371
 
209
372
  for result in maker(
210
- template,
373
+ test_func,
211
374
  settings=settings,
212
375
  generation_config=generation_config,
213
- seed=seed,
376
+ seed=ctx.seed,
214
377
  as_strategy_kwargs=as_strategy_kwargs,
215
378
  ):
216
379
  if isinstance(result, Ok):
217
380
  operation, test = result.ok()
218
- feedback = Feedback(self.stateful, operation)
381
+ if self.stateful is not None and not experimental.STATEFUL_TEST_RUNNER.is_enabled:
382
+ feedback = Feedback(self.stateful, operation)
383
+ else:
384
+ feedback = None
219
385
  # Track whether `BeforeExecution` was already emitted.
220
386
  # Schema error may happen before / after `BeforeExecution`, but it should be emitted only once
221
387
  # and the `AfterExecution` event should have the same correlation id as previous `BeforeExecution`
@@ -224,7 +390,7 @@ class BaseRunner:
224
390
  for event in run_test(
225
391
  operation,
226
392
  test,
227
- results=results,
393
+ ctx=ctx,
228
394
  feedback=feedback,
229
395
  recursion_level=recursion_level,
230
396
  data_generation_methods=self.schema.data_generation_methods,
@@ -237,30 +403,28 @@ class BaseRunner:
237
403
  if isinstance(event, events.Interrupted):
238
404
  return
239
405
  # 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
- )
406
+ if feedback is not None:
407
+ yield from self._run_tests(
408
+ feedback.get_stateful_tests,
409
+ test_func,
410
+ settings=settings,
411
+ generation_config=generation_config,
412
+ recursion_level=recursion_level + 1,
413
+ ctx=ctx,
414
+ headers=headers,
415
+ **kwargs,
416
+ )
251
417
  except OperationSchemaError as exc:
252
418
  yield from handle_schema_error(
253
419
  exc,
254
- results,
420
+ ctx,
255
421
  self.schema.data_generation_methods,
256
422
  recursion_level,
257
423
  before_execution_correlation_id=before_execution_correlation_id,
258
424
  )
259
425
  else:
260
426
  # Schema errors
261
- yield from handle_schema_error(
262
- result.err(), results, self.schema.data_generation_methods, recursion_level
263
- )
427
+ yield from handle_schema_error(result.err(), ctx, self.schema.data_generation_methods, recursion_level)
264
428
 
265
429
 
266
430
  def run_probes(schema: BaseSchema, config: probes.ProbeConfig) -> list[probes.ProbeRun]:
@@ -268,12 +432,9 @@ def run_probes(schema: BaseSchema, config: probes.ProbeConfig) -> list[probes.Pr
268
432
  results = probes.run(schema, config)
269
433
  for result in results:
270
434
  if isinstance(result.probe, probes.NullByteInHeader) and result.is_failure:
271
- from ...specs.openapi._hypothesis import HEADER_FORMAT, header_values
435
+ from ...specs.openapi.formats import HEADER_FORMAT, header_values
272
436
 
273
- formats.register(
274
- HEADER_FORMAT,
275
- header_values(blacklist_characters="\n\r\x00").map(str.lstrip),
276
- )
437
+ formats.register(HEADER_FORMAT, header_values(blacklist_characters="\n\r\x00"))
277
438
  return results
278
439
 
279
440
 
@@ -308,7 +469,7 @@ class EventStream:
308
469
 
309
470
  def handle_schema_error(
310
471
  error: OperationSchemaError,
311
- results: TestResultSet,
472
+ ctx: RunnerContext,
312
473
  data_generation_methods: Iterable[DataGenerationMethod],
313
474
  recursion_level: int,
314
475
  *,
@@ -353,11 +514,11 @@ def handle_schema_error(
353
514
  hypothesis_output=[],
354
515
  correlation_id=correlation_id,
355
516
  )
356
- results.append(result)
517
+ ctx.add_result(result)
357
518
  else:
358
519
  # When there is no `method`, then the schema error may cover multiple operations, and we can't display it in
359
520
  # the progress bar
360
- results.generic_errors.append(error)
521
+ ctx.add_generic_error(error)
361
522
 
362
523
 
363
524
  def run_test(
@@ -366,7 +527,7 @@ def run_test(
366
527
  checks: Iterable[CheckFunction],
367
528
  data_generation_methods: Iterable[DataGenerationMethod],
368
529
  targets: Iterable[Target],
369
- results: TestResultSet,
530
+ ctx: RunnerContext,
370
531
  headers: dict[str, Any] | None,
371
532
  recursion_level: int,
372
533
  **kwargs: Any,
@@ -391,12 +552,35 @@ def run_test(
391
552
  errors: list[Exception] = []
392
553
  test_start_time = time.monotonic()
393
554
  setup_hypothesis_database_key(test, operation)
555
+
556
+ def _on_flaky(exc: Exception) -> Status:
557
+ if isinstance(exc.__cause__, hypothesis.errors.DeadlineExceeded):
558
+ status = Status.error
559
+ result.add_error(DeadlineExceeded.from_exc(exc.__cause__))
560
+ elif (
561
+ hasattr(hypothesis.errors, "FlakyFailure")
562
+ and isinstance(exc, hypothesis.errors.FlakyFailure)
563
+ and any(isinstance(subexc, hypothesis.errors.DeadlineExceeded) for subexc in exc.exceptions)
564
+ ):
565
+ for sub_exc in exc.exceptions:
566
+ if isinstance(sub_exc, hypothesis.errors.DeadlineExceeded):
567
+ result.add_error(DeadlineExceeded.from_exc(sub_exc))
568
+ status = Status.error
569
+ elif errors:
570
+ status = Status.error
571
+ add_errors(result, errors)
572
+ else:
573
+ status = Status.failure
574
+ result.mark_flaky()
575
+ return status
576
+
394
577
  try:
395
578
  with catch_warnings(record=True) as warnings, capture_hypothesis_output() as hypothesis_output:
396
579
  test(
397
- checks,
398
- targets,
399
- result,
580
+ ctx=ctx,
581
+ checks=checks,
582
+ targets=targets,
583
+ result=result,
400
584
  errors=errors,
401
585
  headers=headers,
402
586
  data_generation_methods=data_generation_methods,
@@ -420,6 +604,8 @@ def run_test(
420
604
  result.mark_errored()
421
605
  for error in deduplicate_errors(errors):
422
606
  result.add_error(error)
607
+ except hypothesis.errors.Flaky as exc:
608
+ status = _on_flaky(exc)
423
609
  except MultipleFailures:
424
610
  # Schemathesis may detect multiple errors that come from different check results
425
611
  # They raise different "grouped" exceptions
@@ -428,16 +614,6 @@ def run_test(
428
614
  add_errors(result, errors)
429
615
  else:
430
616
  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
617
  except hypothesis.errors.Unsatisfiable:
442
618
  # We need more clear error message here
443
619
  status = Status.error
@@ -448,13 +624,29 @@ def run_test(
448
624
  except SkipTest as exc:
449
625
  status = Status.skip
450
626
  result.mark_skipped(exc)
451
- except AssertionError: # comes from `hypothesis-jsonschema`
452
- error = reraise(operation)
627
+ except AssertionError as exc: # May come from `hypothesis-jsonschema` or `hypothesis`
453
628
  status = Status.error
629
+ try:
630
+ operation.schema.validate()
631
+ msg = "Unexpected error during testing of this API operation"
632
+ exc_msg = str(exc)
633
+ if exc_msg:
634
+ msg += f": {exc_msg}"
635
+ try:
636
+ raise InternalError(msg) from exc
637
+ except InternalError as exc:
638
+ error = exc
639
+ except ValidationError as exc:
640
+ error = OperationSchemaError.from_jsonschema_error(
641
+ exc,
642
+ path=operation.path,
643
+ method=operation.method,
644
+ full_path=operation.schema.get_full_path(operation.path),
645
+ )
454
646
  result.add_error(error)
455
647
  except HypothesisRefResolutionError:
456
648
  status = Status.error
457
- result.add_error(hypothesis.errors.Unsatisfiable(RECURSIVE_REFERENCE_ERROR_MESSAGE))
649
+ result.add_error(RecursiveReferenceError(RECURSIVE_REFERENCE_ERROR_MESSAGE))
458
650
  except InvalidArgument as error:
459
651
  status = Status.error
460
652
  message = get_invalid_regular_expression_message(warnings)
@@ -482,6 +674,8 @@ def run_test(
482
674
  )
483
675
  else:
484
676
  result.add_error(error)
677
+ if status == Status.success and ctx.no_failfast and any(check.value == Status.failure for check in result.checks):
678
+ status = Status.failure
485
679
  if has_unsatisfied_example_mark(test):
486
680
  status = Status.error
487
681
  result.add_error(
@@ -513,10 +707,10 @@ def run_test(
513
707
  result.seed = getattr(test, "_hypothesis_internal_use_seed", None) or getattr(
514
708
  test, "_hypothesis_internal_use_generated_seed", None
515
709
  )
516
- results.append(result)
710
+ ctx.add_result(result)
517
711
  for status_code in (401, 403):
518
712
  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))
713
+ ctx.add_warning(TOO_MANY_RESPONSES_WARNING_TEMPLATE.format(f"`{operation.verbose_name}`", status_code))
520
714
  yield events.AfterExecution.from_result(
521
715
  result=result,
522
716
  status=status,
@@ -551,22 +745,6 @@ def has_too_many_responses_with_status(result: TestResult, status_code: int) ->
551
745
  ALL_NOT_FOUND_WARNING_MESSAGE = "All API responses have a 404 status code. Did you specify the proper API location?"
552
746
 
553
747
 
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
748
  def setup_hypothesis_database_key(test: Callable, operation: APIOperation) -> None:
571
749
  """Make Hypothesis use separate database entries for every API operation.
572
750
 
@@ -575,7 +753,7 @@ def setup_hypothesis_database_key(test: Callable, operation: APIOperation) -> No
575
753
  # Hypothesis's function digest depends on the test function signature. To reflect it for the web API case,
576
754
  # we use all API operation parameters in the digest.
577
755
  extra = operation.verbose_name.encode("utf8")
578
- for parameter in operation.definition.parameters:
756
+ for parameter in operation.iter_parameters():
579
757
  extra += parameter.serialize(operation).encode("utf8")
580
758
  test.hypothesis.inner_test._hypothesis_internal_add_digest = extra # type: ignore
581
759
 
@@ -588,16 +766,6 @@ def get_invalid_regular_expression_message(warnings: list[WarningMessage]) -> st
588
766
  return None
589
767
 
590
768
 
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
769
  MEMORY_ADDRESS_RE = re.compile("0x[0-9a-fA-F]+")
602
770
  URL_IN_ERROR_MESSAGE_RE = re.compile(r"Max retries exceeded with url: .*? \(Caused by")
603
771
 
@@ -613,7 +781,9 @@ def group_errors(errors: list[Exception]) -> None:
613
781
  serialization_errors = [error for error in errors if isinstance(error, SerializationNotPossible)]
614
782
  if len(serialization_errors) > 1:
615
783
  errors[:] = [error for error in errors if not isinstance(error, SerializationNotPossible)]
616
- media_types = sum((entry.media_types for entry in serialization_errors), [])
784
+ media_types: list[str] = functools.reduce(
785
+ operator.iadd, (entry.media_types for entry in serialization_errors), []
786
+ )
617
787
  errors.append(SerializationNotPossible.from_media_types(*media_types))
618
788
 
619
789
 
@@ -638,12 +808,14 @@ def deduplicate_errors(errors: list[Exception]) -> Generator[Exception, None, No
638
808
  def run_checks(
639
809
  *,
640
810
  case: Case,
811
+ ctx: CheckContext,
641
812
  checks: Iterable[CheckFunction],
642
813
  check_results: list[Check],
643
814
  result: TestResult,
644
815
  response: GenericResponse,
645
816
  elapsed_time: float,
646
817
  max_response_time: int | None = None,
818
+ no_failfast: bool,
647
819
  ) -> None:
648
820
  errors = []
649
821
 
@@ -660,7 +832,7 @@ def run_checks(
660
832
  check_name = check.__name__
661
833
  copied_case = case.partial_deepcopy()
662
834
  try:
663
- skip_check = check(response, copied_case)
835
+ skip_check = check(ctx, response, copied_case)
664
836
  if not skip_check:
665
837
  check_result = result.add_success(check_name, copied_case, response, elapsed_time)
666
838
  check_results.append(check_result)
@@ -672,7 +844,7 @@ def run_checks(
672
844
 
673
845
  if max_response_time:
674
846
  if elapsed_time > max_response_time:
675
- message = f"Actual: {elapsed_time:.2f}ms\nLimit: {max_response_time}.00ms"
847
+ message = _make_max_response_time_failure_message(elapsed_time, max_response_time)
676
848
  errors.append(AssertionError(message))
677
849
  result.add_failure(
678
850
  "max_response_time",
@@ -685,7 +857,7 @@ def run_checks(
685
857
  else:
686
858
  result.add_success("max_response_time", case, response, elapsed_time)
687
859
 
688
- if errors:
860
+ if errors and not no_failfast:
689
861
  raise get_grouped_exception(case.operation.verbose_name, *errors)(causes=tuple(errors))
690
862
 
691
863
 
@@ -746,19 +918,42 @@ def _force_data_generation_method(values: list[DataGenerationMethod], case: Case
746
918
  values[:] = [data_generation_method]
747
919
 
748
920
 
921
+ def cached_test_func(f: Callable) -> Callable:
922
+ def wrapped(*, ctx: RunnerContext, case: Case, **kwargs: Any) -> None:
923
+ if ctx.unique_data:
924
+ cached = ctx.get_cached_outcome(case)
925
+ if isinstance(cached, BaseException):
926
+ raise cached
927
+ elif cached is None:
928
+ return None
929
+ try:
930
+ f(ctx=ctx, case=case, **kwargs)
931
+ except BaseException as exc:
932
+ ctx.cache_outcome(case, exc)
933
+ raise
934
+ else:
935
+ ctx.cache_outcome(case, None)
936
+ else:
937
+ f(ctx=ctx, case=case, **kwargs)
938
+
939
+ wrapped.__name__ = f.__name__
940
+
941
+ return wrapped
942
+
943
+
944
+ @cached_test_func
749
945
  def network_test(
946
+ *,
947
+ ctx: RunnerContext,
750
948
  case: Case,
751
949
  checks: Iterable[CheckFunction],
752
950
  targets: Iterable[Target],
753
951
  result: TestResult,
754
952
  session: requests.Session,
755
- request_timeout: int | None,
756
- request_tls_verify: bool,
757
- request_proxy: str | None,
758
- request_cert: RequestCert | None,
953
+ request_config: RequestConfig,
759
954
  store_interactions: bool,
760
955
  headers: dict[str, Any] | None,
761
- feedback: Feedback,
956
+ feedback: Feedback | None,
762
957
  max_response_time: int | None,
763
958
  data_generation_methods: list[DataGenerationMethod],
764
959
  dry_run: bool,
@@ -771,85 +966,97 @@ def network_test(
771
966
  headers = headers or {}
772
967
  if "user-agent" not in {header.lower() for header in headers}:
773
968
  headers["User-Agent"] = USER_AGENT
774
- timeout = prepare_timeout(request_timeout)
775
969
  if not dry_run:
776
970
  args = (
971
+ ctx,
777
972
  checks,
778
973
  targets,
779
974
  result,
780
975
  session,
781
- timeout,
976
+ request_config,
782
977
  store_interactions,
783
978
  headers,
784
979
  feedback,
785
- request_tls_verify,
786
- request_proxy,
787
- request_cert,
788
980
  max_response_time,
789
981
  )
790
982
  response = _network_test(case, *args)
791
983
  add_cases(case, response, _network_test, *args)
984
+ elif store_interactions:
985
+ result.store_requests_response(case, None, Status.skip, [], headers=headers, session=session)
792
986
 
793
987
 
794
988
  def _network_test(
795
989
  case: Case,
990
+ ctx: RunnerContext,
796
991
  checks: Iterable[CheckFunction],
797
992
  targets: Iterable[Target],
798
993
  result: TestResult,
799
994
  session: requests.Session,
800
- timeout: float | None,
995
+ request_config: RequestConfig,
801
996
  store_interactions: bool,
802
997
  headers: dict[str, Any] | None,
803
- feedback: Feedback,
804
- request_tls_verify: bool,
805
- request_proxy: str | None,
806
- request_cert: RequestCert | None,
998
+ feedback: Feedback | None,
807
999
  max_response_time: int | None,
808
1000
  ) -> requests.Response:
809
1001
  check_results: list[Check] = []
1002
+ hook_context = HookContext(operation=case.operation)
1003
+ kwargs: dict[str, Any] = {
1004
+ "session": session,
1005
+ "headers": headers,
1006
+ "timeout": request_config.prepared_timeout,
1007
+ "verify": request_config.tls_verify,
1008
+ "cert": request_config.cert,
1009
+ }
1010
+ if request_config.proxy is not None:
1011
+ kwargs["proxies"] = {"all": request_config.proxy}
1012
+ hooks.dispatch("process_call_kwargs", hook_context, case, kwargs)
810
1013
  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
1014
  response = case.call(**kwargs)
823
1015
  except CheckFailed as exc:
824
1016
  check_name = "request_timeout"
825
- requests_kwargs = case.as_requests_kwargs(base_url=case.get_full_base_url(), headers=headers)
1017
+ requests_kwargs = RequestsTransport().serialize_case(case, base_url=case.get_full_base_url(), headers=headers)
826
1018
  request = requests.Request(**requests_kwargs).prepare()
827
- elapsed = cast(float, timeout) # It is defined and not empty, since the exception happened
1019
+ elapsed = cast(
1020
+ float, request_config.prepared_timeout
1021
+ ) # It is defined and not empty, since the exception happened
828
1022
  check_result = result.add_failure(
829
1023
  check_name, case, None, elapsed, f"Response timed out after {1000 * elapsed:.2f}ms", exc.context, request
830
1024
  )
831
1025
  check_results.append(check_result)
1026
+ if store_interactions:
1027
+ result.store_requests_response(case, None, Status.failure, [check_result], headers=headers, session=session)
832
1028
  raise exc
833
1029
  context = TargetContext(case=case, response=response, response_time=response.elapsed.total_seconds())
834
1030
  run_targets(targets, context)
835
1031
  status = Status.success
1032
+
1033
+ check_ctx = CheckContext(
1034
+ override=ctx.override,
1035
+ auth=ctx.auth,
1036
+ headers=CaseInsensitiveDict(headers) if headers else None,
1037
+ config=ctx.checks_config,
1038
+ transport_kwargs=kwargs,
1039
+ )
836
1040
  try:
837
1041
  run_checks(
838
1042
  case=case,
1043
+ ctx=check_ctx,
839
1044
  checks=checks,
840
1045
  check_results=check_results,
841
1046
  result=result,
842
1047
  response=response,
843
1048
  elapsed_time=context.response_time * 1000,
844
1049
  max_response_time=max_response_time,
1050
+ no_failfast=ctx.no_failfast,
845
1051
  )
846
1052
  except CheckFailed:
847
1053
  status = Status.failure
848
1054
  raise
849
1055
  finally:
850
- feedback.add_test_case(case, response)
1056
+ if feedback is not None:
1057
+ feedback.add_test_case(case, response)
851
1058
  if store_interactions:
852
- result.store_requests_response(case, response, status, check_results)
1059
+ result.store_requests_response(case, response, status, check_results, headers=headers, session=session)
853
1060
  return response
854
1061
 
855
1062
 
@@ -861,15 +1068,9 @@ def get_session(auth: HTTPDigestAuth | RawAuth | None = None) -> Generator[reque
861
1068
  yield session
862
1069
 
863
1070
 
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
-
1071
+ @cached_test_func
872
1072
  def wsgi_test(
1073
+ ctx: RunnerContext,
873
1074
  case: Case,
874
1075
  checks: Iterable[CheckFunction],
875
1076
  targets: Iterable[Target],
@@ -878,7 +1079,7 @@ def wsgi_test(
878
1079
  auth_type: str | None,
879
1080
  headers: dict[str, Any] | None,
880
1081
  store_interactions: bool,
881
- feedback: Feedback,
1082
+ feedback: Feedback | None,
882
1083
  max_response_time: int | None,
883
1084
  data_generation_methods: list[DataGenerationMethod],
884
1085
  dry_run: bool,
@@ -887,9 +1088,10 @@ def wsgi_test(
887
1088
  with ErrorCollector(errors):
888
1089
  _force_data_generation_method(data_generation_methods, case)
889
1090
  result.mark_executed()
890
- headers = _prepare_wsgi_headers(headers, auth, auth_type)
1091
+ headers = prepare_wsgi_headers(headers, auth, auth_type)
891
1092
  if not dry_run:
892
1093
  args = (
1094
+ ctx,
893
1095
  checks,
894
1096
  targets,
895
1097
  result,
@@ -900,78 +1102,73 @@ def wsgi_test(
900
1102
  )
901
1103
  response = _wsgi_test(case, *args)
902
1104
  add_cases(case, response, _wsgi_test, *args)
1105
+ elif store_interactions:
1106
+ result.store_wsgi_response(case, None, headers, None, Status.skip, [])
903
1107
 
904
1108
 
905
1109
  def _wsgi_test(
906
1110
  case: Case,
1111
+ ctx: RunnerContext,
907
1112
  checks: Iterable[CheckFunction],
908
1113
  targets: Iterable[Target],
909
1114
  result: TestResult,
910
1115
  headers: dict[str, Any],
911
1116
  store_interactions: bool,
912
- feedback: Feedback,
1117
+ feedback: Feedback | None,
913
1118
  max_response_time: int | None,
914
1119
  ) -> WSGIResponse:
1120
+ from ...transports.responses import WSGIResponse
1121
+
915
1122
  with catching_logs(LogCaptureHandler(), level=logging.DEBUG) as recorded:
916
- start = time.monotonic()
917
1123
  hook_context = HookContext(operation=case.operation)
918
- kwargs = {"headers": headers}
1124
+ kwargs: dict[str, Any] = {"headers": headers}
919
1125
  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)
1126
+ response = cast(WSGIResponse, case.call(**kwargs))
1127
+ context = TargetContext(case=case, response=response, response_time=response.elapsed.total_seconds())
923
1128
  run_targets(targets, context)
924
1129
  result.logs.extend(recorded.records)
925
1130
  status = Status.success
926
1131
  check_results: list[Check] = []
1132
+ check_ctx = CheckContext(
1133
+ override=ctx.override,
1134
+ auth=ctx.auth,
1135
+ headers=CaseInsensitiveDict(headers) if headers else None,
1136
+ config=ctx.checks_config,
1137
+ transport_kwargs=kwargs,
1138
+ )
927
1139
  try:
928
1140
  run_checks(
929
1141
  case=case,
1142
+ ctx=check_ctx,
930
1143
  checks=checks,
931
1144
  check_results=check_results,
932
1145
  result=result,
933
1146
  response=response,
934
1147
  elapsed_time=context.response_time * 1000,
935
1148
  max_response_time=max_response_time,
1149
+ no_failfast=ctx.no_failfast,
936
1150
  )
937
1151
  except CheckFailed:
938
1152
  status = Status.failure
939
1153
  raise
940
1154
  finally:
941
- feedback.add_test_case(case, response)
1155
+ if feedback is not None:
1156
+ feedback.add_test_case(case, response)
942
1157
  if store_interactions:
943
- result.store_wsgi_response(case, response, headers, elapsed, status, check_results)
1158
+ result.store_wsgi_response(case, response, headers, response.elapsed.total_seconds(), status, check_results)
944
1159
  return response
945
1160
 
946
1161
 
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
-
1162
+ @cached_test_func
967
1163
  def asgi_test(
1164
+ ctx: RunnerContext,
968
1165
  case: Case,
969
1166
  checks: Iterable[CheckFunction],
970
1167
  targets: Iterable[Target],
971
1168
  result: TestResult,
972
1169
  store_interactions: bool,
973
1170
  headers: dict[str, Any] | None,
974
- feedback: Feedback,
1171
+ feedback: Feedback | None,
975
1172
  max_response_time: int | None,
976
1173
  data_generation_methods: list[DataGenerationMethod],
977
1174
  dry_run: bool,
@@ -985,6 +1182,7 @@ def asgi_test(
985
1182
 
986
1183
  if not dry_run:
987
1184
  args = (
1185
+ ctx,
988
1186
  checks,
989
1187
  targets,
990
1188
  result,
@@ -995,41 +1193,54 @@ def asgi_test(
995
1193
  )
996
1194
  response = _asgi_test(case, *args)
997
1195
  add_cases(case, response, _asgi_test, *args)
1196
+ elif store_interactions:
1197
+ result.store_requests_response(case, None, Status.skip, [], headers=headers, session=None)
998
1198
 
999
1199
 
1000
1200
  def _asgi_test(
1001
1201
  case: Case,
1202
+ ctx: RunnerContext,
1002
1203
  checks: Iterable[CheckFunction],
1003
1204
  targets: Iterable[Target],
1004
1205
  result: TestResult,
1005
1206
  store_interactions: bool,
1006
1207
  headers: dict[str, Any] | None,
1007
- feedback: Feedback,
1208
+ feedback: Feedback | None,
1008
1209
  max_response_time: int | None,
1009
1210
  ) -> requests.Response:
1010
1211
  hook_context = HookContext(operation=case.operation)
1011
1212
  kwargs: dict[str, Any] = {"headers": headers}
1012
1213
  hooks.dispatch("process_call_kwargs", hook_context, case, kwargs)
1013
- response = case.call_asgi(**kwargs)
1214
+ response = case.call(**kwargs)
1014
1215
  context = TargetContext(case=case, response=response, response_time=response.elapsed.total_seconds())
1015
1216
  run_targets(targets, context)
1016
1217
  status = Status.success
1017
1218
  check_results: list[Check] = []
1219
+ check_ctx = CheckContext(
1220
+ override=ctx.override,
1221
+ auth=ctx.auth,
1222
+ headers=CaseInsensitiveDict(headers) if headers else None,
1223
+ config=ctx.checks_config,
1224
+ transport_kwargs=kwargs,
1225
+ )
1018
1226
  try:
1019
1227
  run_checks(
1020
1228
  case=case,
1229
+ ctx=check_ctx,
1021
1230
  checks=checks,
1022
1231
  check_results=check_results,
1023
1232
  result=result,
1024
1233
  response=response,
1025
1234
  elapsed_time=context.response_time * 1000,
1026
1235
  max_response_time=max_response_time,
1236
+ no_failfast=ctx.no_failfast,
1027
1237
  )
1028
1238
  except CheckFailed:
1029
1239
  status = Status.failure
1030
1240
  raise
1031
1241
  finally:
1032
- feedback.add_test_case(case, response)
1242
+ if feedback is not None:
1243
+ feedback.add_test_case(case, response)
1033
1244
  if store_interactions:
1034
- result.store_requests_response(case, response, status, check_results)
1245
+ result.store_requests_response(case, response, status, check_results, headers, session=None)
1035
1246
  return response