schemathesis 3.35.4__py3-none-any.whl → 3.36.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. schemathesis/__init__.py +5 -5
  2. schemathesis/_hypothesis.py +12 -6
  3. schemathesis/_override.py +4 -4
  4. schemathesis/auths.py +1 -1
  5. schemathesis/checks.py +8 -5
  6. schemathesis/cli/__init__.py +23 -26
  7. schemathesis/cli/callbacks.py +6 -4
  8. schemathesis/cli/cassettes.py +67 -41
  9. schemathesis/cli/context.py +7 -6
  10. schemathesis/cli/junitxml.py +1 -1
  11. schemathesis/cli/options.py +7 -4
  12. schemathesis/cli/output/default.py +5 -5
  13. schemathesis/cli/reporting.py +4 -2
  14. schemathesis/code_samples.py +4 -3
  15. schemathesis/contrib/unique_data.py +1 -2
  16. schemathesis/exceptions.py +4 -3
  17. schemathesis/extra/_flask.py +4 -1
  18. schemathesis/extra/pytest_plugin.py +6 -3
  19. schemathesis/failures.py +2 -1
  20. schemathesis/filters.py +2 -2
  21. schemathesis/generation/__init__.py +2 -2
  22. schemathesis/generation/_hypothesis.py +1 -1
  23. schemathesis/generation/coverage.py +53 -12
  24. schemathesis/graphql.py +0 -1
  25. schemathesis/hooks.py +3 -3
  26. schemathesis/internal/checks.py +53 -0
  27. schemathesis/lazy.py +10 -7
  28. schemathesis/loaders.py +3 -3
  29. schemathesis/models.py +59 -23
  30. schemathesis/runner/__init__.py +12 -6
  31. schemathesis/runner/events.py +1 -1
  32. schemathesis/runner/impl/context.py +72 -0
  33. schemathesis/runner/impl/core.py +105 -67
  34. schemathesis/runner/impl/solo.py +17 -20
  35. schemathesis/runner/impl/threadpool.py +65 -72
  36. schemathesis/runner/serialization.py +4 -3
  37. schemathesis/sanitization.py +2 -1
  38. schemathesis/schemas.py +20 -22
  39. schemathesis/serializers.py +2 -0
  40. schemathesis/service/client.py +1 -1
  41. schemathesis/service/events.py +4 -1
  42. schemathesis/service/extensions.py +2 -2
  43. schemathesis/service/hosts.py +4 -2
  44. schemathesis/service/models.py +3 -3
  45. schemathesis/service/report.py +3 -3
  46. schemathesis/service/serialization.py +4 -2
  47. schemathesis/specs/graphql/loaders.py +5 -4
  48. schemathesis/specs/graphql/schemas.py +13 -8
  49. schemathesis/specs/openapi/checks.py +76 -27
  50. schemathesis/specs/openapi/definitions.py +1 -5
  51. schemathesis/specs/openapi/examples.py +92 -2
  52. schemathesis/specs/openapi/expressions/__init__.py +7 -0
  53. schemathesis/specs/openapi/expressions/extractors.py +4 -1
  54. schemathesis/specs/openapi/expressions/nodes.py +5 -3
  55. schemathesis/specs/openapi/links.py +4 -4
  56. schemathesis/specs/openapi/loaders.py +6 -5
  57. schemathesis/specs/openapi/negative/__init__.py +5 -3
  58. schemathesis/specs/openapi/negative/mutations.py +5 -4
  59. schemathesis/specs/openapi/parameters.py +4 -2
  60. schemathesis/specs/openapi/schemas.py +28 -13
  61. schemathesis/specs/openapi/security.py +6 -4
  62. schemathesis/specs/openapi/stateful/__init__.py +2 -2
  63. schemathesis/specs/openapi/stateful/statistic.py +3 -3
  64. schemathesis/specs/openapi/stateful/types.py +3 -2
  65. schemathesis/stateful/__init__.py +3 -3
  66. schemathesis/stateful/config.py +2 -1
  67. schemathesis/stateful/context.py +13 -3
  68. schemathesis/stateful/events.py +3 -3
  69. schemathesis/stateful/runner.py +24 -6
  70. schemathesis/stateful/sink.py +1 -1
  71. schemathesis/stateful/state_machine.py +7 -6
  72. schemathesis/stateful/statistic.py +3 -1
  73. schemathesis/stateful/validation.py +10 -5
  74. schemathesis/transports/__init__.py +2 -2
  75. schemathesis/transports/asgi.py +7 -0
  76. schemathesis/transports/auth.py +2 -1
  77. schemathesis/transports/content_types.py +1 -1
  78. schemathesis/transports/responses.py +2 -1
  79. schemathesis/utils.py +4 -2
  80. {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/METADATA +1 -1
  81. schemathesis-3.36.0.dist-info/RECORD +157 -0
  82. schemathesis-3.35.4.dist-info/RECORD +0 -154
  83. {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/WHEEL +0 -0
  84. {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/entry_points.txt +0 -0
  85. {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,8 @@
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
@@ -9,7 +11,6 @@ import uuid
9
11
  import warnings
10
12
  from contextlib import contextmanager
11
13
  from dataclasses import dataclass, field
12
- from types import TracebackType
13
14
  from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, List, Literal, cast
14
15
  from warnings import WarningMessage, catch_warnings
15
16
 
@@ -20,7 +21,7 @@ from hypothesis.errors import HypothesisException, InvalidArgument
20
21
  from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
21
22
  from jsonschema.exceptions import SchemaError as JsonSchemaError
22
23
  from jsonschema.exceptions import ValidationError
23
- from requests.auth import HTTPDigestAuth
24
+ from requests.structures import CaseInsensitiveDict
24
25
  from urllib3.exceptions import InsecureRequestWarning
25
26
 
26
27
  from ... import experimental, failures, hooks
@@ -31,7 +32,6 @@ from ..._hypothesis import (
31
32
  get_non_serializable_mark,
32
33
  has_unsatisfied_example_mark,
33
34
  )
34
- from ..._override import CaseOverride
35
35
  from ...auths import unregister as unregister_auth
36
36
  from ...checks import _make_max_response_time_failure_message
37
37
  from ...constants import (
@@ -57,11 +57,11 @@ from ...exceptions import (
57
57
  )
58
58
  from ...generation import DataGenerationMethod, GenerationConfig
59
59
  from ...hooks import HookContext, get_all_by_name
60
+ from ...internal.checks import CheckContext
60
61
  from ...internal.datetime import current_datetime
61
62
  from ...internal.result import Err, Ok, Result
62
- from ...models import APIOperation, Case, Check, CheckFunction, Status, TestResult, TestResultSet
63
+ from ...models import APIOperation, Case, Check, Status, TestResult
63
64
  from ...runner import events
64
- from ...schemas import BaseSchema
65
65
  from ...service import extensions
66
66
  from ...service.models import AnalysisResult, AnalysisSuccess
67
67
  from ...specs.openapi import formats
@@ -71,14 +71,22 @@ from ...stateful import runner as stateful_runner
71
71
  from ...targets import Target, TargetContext
72
72
  from ...transports import RequestConfig, RequestsTransport
73
73
  from ...transports.auth import get_requests_auth, prepare_wsgi_headers
74
- from ...types import RawAuth
75
74
  from ...utils import capture_hypothesis_output
76
75
  from .. import probes
77
76
  from ..serialization import SerializedTestResult
77
+ from .context import RunnerContext
78
78
 
79
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
80
87
  from ...service.client import ServiceClient
81
88
  from ...transports.responses import GenericResponse, WSGIResponse
89
+ from ...types import RawAuth
82
90
 
83
91
 
84
92
  def _should_count_towards_stop(event: events.ExecutionEvent) -> bool:
@@ -92,7 +100,7 @@ class BaseRunner:
92
100
  max_response_time: int | None
93
101
  targets: Iterable[Target]
94
102
  hypothesis_settings: hypothesis.settings
95
- generation_config: GenerationConfig
103
+ generation_config: GenerationConfig | None
96
104
  probe_config: probes.ProbeConfig
97
105
  request_config: RequestConfig = field(default_factory=RequestConfig)
98
106
  override: CaseOverride | None = None
@@ -104,6 +112,7 @@ class BaseRunner:
104
112
  exit_first: bool = False
105
113
  max_failures: int | None = None
106
114
  started_at: str = field(default_factory=current_datetime)
115
+ unique_data: bool = False
107
116
  dry_run: bool = False
108
117
  stateful: Stateful | None = None
109
118
  stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT
@@ -122,7 +131,7 @@ class BaseRunner:
122
131
  # If auth is explicitly provided, then the global provider is ignored
123
132
  if self.auth is not None:
124
133
  unregister_auth()
125
- results = TestResultSet(seed=self.seed)
134
+ ctx = RunnerContext(seed=self.seed, stop_event=stop_event, unique_data=self.unique_data)
126
135
  start_time = time.monotonic()
127
136
  initialized = None
128
137
  __probes = None
@@ -134,15 +143,15 @@ class BaseRunner:
134
143
  schema=self.schema,
135
144
  count_operations=self.count_operations,
136
145
  count_links=self.count_links,
137
- seed=self.seed,
146
+ seed=ctx.seed,
138
147
  start_time=start_time,
139
148
  )
140
149
  return initialized
141
150
 
142
151
  def _finish() -> events.Finished:
143
- if has_all_not_found(results):
144
- results.add_warning(ALL_NOT_FOUND_WARNING_MESSAGE)
145
- return events.Finished.from_results(results=results, running_time=time.monotonic() - start_time)
152
+ if ctx.has_all_not_found:
153
+ ctx.add_warning(ALL_NOT_FOUND_WARNING_MESSAGE)
154
+ return events.Finished.from_results(results=ctx.data, running_time=time.monotonic() - start_time)
146
155
 
147
156
  def _before_probes() -> events.BeforeProbing:
148
157
  return events.BeforeProbing()
@@ -176,7 +185,7 @@ class BaseRunner:
176
185
  def _after_analysis() -> events.AfterAnalysis:
177
186
  return events.AfterAnalysis(analysis=__analysis)
178
187
 
179
- if stop_event.is_set():
188
+ if ctx.is_stopped:
180
189
  yield _finish()
181
190
  return
182
191
 
@@ -192,16 +201,16 @@ class BaseRunner:
192
201
  event = event_factory()
193
202
  if event is not None:
194
203
  yield event
195
- if stop_event.is_set():
196
- yield _finish()
204
+ if ctx.is_stopped:
205
+ yield _finish() # type: ignore[unreachable]
197
206
  return
198
207
 
199
208
  try:
200
209
  warnings.simplefilter("ignore", InsecureRequestWarning)
201
210
  if not experimental.STATEFUL_ONLY.is_enabled:
202
- yield from self._execute(results, stop_event)
211
+ yield from self._execute(ctx)
203
212
  if not self._is_stopping_due_to_failure_limit:
204
- yield from self._run_stateful_tests(results)
213
+ yield from self._run_stateful_tests(ctx)
205
214
  except KeyboardInterrupt:
206
215
  yield events.Interrupted()
207
216
 
@@ -222,19 +231,17 @@ class BaseRunner:
222
231
  return self._failures_counter >= self.max_failures
223
232
  return False
224
233
 
225
- def _execute(
226
- self, results: TestResultSet, stop_event: threading.Event
227
- ) -> Generator[events.ExecutionEvent, None, None]:
234
+ def _execute(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
228
235
  raise NotImplementedError
229
236
 
230
- def _run_stateful_tests(self, results: TestResultSet) -> Generator[events.ExecutionEvent, None, None]:
237
+ def _run_stateful_tests(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
231
238
  # Run new-style stateful tests
232
239
  if self.stateful is not None and experimental.STATEFUL_TEST_RUNNER.is_enabled and self.schema.links_count > 0:
233
240
  result = TestResult(
234
241
  method="",
235
242
  path="",
236
243
  verbose_name="Stateful tests",
237
- seed=self.seed,
244
+ seed=ctx.seed,
238
245
  data_generation_method=self.schema.data_generation_methods,
239
246
  )
240
247
  headers = self.headers or {}
@@ -251,7 +258,7 @@ class BaseRunner:
251
258
  max_failures=None if self.max_failures is None else self.max_failures - self._failures_counter,
252
259
  request=self.request_config,
253
260
  auth=auth,
254
- seed=self.seed,
261
+ seed=ctx.seed,
255
262
  override=self.override,
256
263
  )
257
264
  state_machine = self.schema.as_state_machine()
@@ -277,6 +284,8 @@ class BaseRunner:
277
284
  case=event.case,
278
285
  response=response,
279
286
  checks=event.checks,
287
+ headers=headers,
288
+ session=None,
280
289
  )
281
290
 
282
291
  else:
@@ -317,7 +326,7 @@ class BaseRunner:
317
326
  status = Status.error
318
327
  result.add_error(stateful_event.exception)
319
328
  yield events.StatefulEvent(data=stateful_event)
320
- results.append(result)
329
+ ctx.add_result(result)
321
330
  yield events.AfterStatefulExecution(
322
331
  status=status,
323
332
  result=SerializedTestResult.from_test_result(result),
@@ -328,11 +337,10 @@ class BaseRunner:
328
337
  def _run_tests(
329
338
  self,
330
339
  maker: Callable,
331
- template: Callable,
340
+ test_func: Callable,
332
341
  settings: hypothesis.settings,
333
- generation_config: GenerationConfig,
334
- seed: int | None,
335
- results: TestResultSet,
342
+ generation_config: GenerationConfig | None,
343
+ ctx: RunnerContext,
336
344
  recursion_level: int = 0,
337
345
  headers: dict[str, Any] | None = None,
338
346
  **kwargs: Any,
@@ -352,10 +360,10 @@ class BaseRunner:
352
360
  return kw
353
361
 
354
362
  for result in maker(
355
- template,
363
+ test_func,
356
364
  settings=settings,
357
365
  generation_config=generation_config,
358
- seed=seed,
366
+ seed=ctx.seed,
359
367
  as_strategy_kwargs=as_strategy_kwargs,
360
368
  ):
361
369
  if isinstance(result, Ok):
@@ -372,7 +380,7 @@ class BaseRunner:
372
380
  for event in run_test(
373
381
  operation,
374
382
  test,
375
- results=results,
383
+ ctx=ctx,
376
384
  feedback=feedback,
377
385
  recursion_level=recursion_level,
378
386
  data_generation_methods=self.schema.data_generation_methods,
@@ -388,28 +396,25 @@ class BaseRunner:
388
396
  if feedback is not None:
389
397
  yield from self._run_tests(
390
398
  feedback.get_stateful_tests,
391
- template,
399
+ test_func,
392
400
  settings=settings,
393
401
  generation_config=generation_config,
394
- seed=seed,
395
402
  recursion_level=recursion_level + 1,
396
- results=results,
403
+ ctx=ctx,
397
404
  headers=headers,
398
405
  **kwargs,
399
406
  )
400
407
  except OperationSchemaError as exc:
401
408
  yield from handle_schema_error(
402
409
  exc,
403
- results,
410
+ ctx,
404
411
  self.schema.data_generation_methods,
405
412
  recursion_level,
406
413
  before_execution_correlation_id=before_execution_correlation_id,
407
414
  )
408
415
  else:
409
416
  # Schema errors
410
- yield from handle_schema_error(
411
- result.err(), results, self.schema.data_generation_methods, recursion_level
412
- )
417
+ yield from handle_schema_error(result.err(), ctx, self.schema.data_generation_methods, recursion_level)
413
418
 
414
419
 
415
420
  def run_probes(schema: BaseSchema, config: probes.ProbeConfig) -> list[probes.ProbeRun]:
@@ -454,7 +459,7 @@ class EventStream:
454
459
 
455
460
  def handle_schema_error(
456
461
  error: OperationSchemaError,
457
- results: TestResultSet,
462
+ ctx: RunnerContext,
458
463
  data_generation_methods: Iterable[DataGenerationMethod],
459
464
  recursion_level: int,
460
465
  *,
@@ -499,11 +504,11 @@ def handle_schema_error(
499
504
  hypothesis_output=[],
500
505
  correlation_id=correlation_id,
501
506
  )
502
- results.append(result)
507
+ ctx.add_result(result)
503
508
  else:
504
509
  # When there is no `method`, then the schema error may cover multiple operations, and we can't display it in
505
510
  # the progress bar
506
- results.generic_errors.append(error)
511
+ ctx.add_generic_error(error)
507
512
 
508
513
 
509
514
  def run_test(
@@ -512,7 +517,7 @@ def run_test(
512
517
  checks: Iterable[CheckFunction],
513
518
  data_generation_methods: Iterable[DataGenerationMethod],
514
519
  targets: Iterable[Target],
515
- results: TestResultSet,
520
+ ctx: RunnerContext,
516
521
  headers: dict[str, Any] | None,
517
522
  recursion_level: int,
518
523
  **kwargs: Any,
@@ -562,9 +567,10 @@ def run_test(
562
567
  try:
563
568
  with catch_warnings(record=True) as warnings, capture_hypothesis_output() as hypothesis_output:
564
569
  test(
565
- checks,
566
- targets,
567
- result,
570
+ ctx=ctx,
571
+ checks=checks,
572
+ targets=targets,
573
+ result=result,
568
574
  errors=errors,
569
575
  headers=headers,
570
576
  data_generation_methods=data_generation_methods,
@@ -689,10 +695,10 @@ def run_test(
689
695
  result.seed = getattr(test, "_hypothesis_internal_use_seed", None) or getattr(
690
696
  test, "_hypothesis_internal_use_generated_seed", None
691
697
  )
692
- results.append(result)
698
+ ctx.add_result(result)
693
699
  for status_code in (401, 403):
694
700
  if has_too_many_responses_with_status(result, status_code):
695
- results.add_warning(TOO_MANY_RESPONSES_WARNING_TEMPLATE.format(f"`{operation.verbose_name}`", status_code))
701
+ ctx.add_warning(TOO_MANY_RESPONSES_WARNING_TEMPLATE.format(f"`{operation.verbose_name}`", status_code))
696
702
  yield events.AfterExecution.from_result(
697
703
  result=result,
698
704
  status=status,
@@ -727,22 +733,6 @@ def has_too_many_responses_with_status(result: TestResult, status_code: int) ->
727
733
  ALL_NOT_FOUND_WARNING_MESSAGE = "All API responses have a 404 status code. Did you specify the proper API location?"
728
734
 
729
735
 
730
- def has_all_not_found(results: TestResultSet) -> bool:
731
- """Check if all responses are 404."""
732
- has_not_found = False
733
- for result in results.results:
734
- for check in result.checks:
735
- if check.response is not None:
736
- if check.response.status_code == 404:
737
- has_not_found = True
738
- else:
739
- # There are non-404 responses, no reason to check any other response
740
- return False
741
- # Only happens if all responses are 404, or there are no responses at all.
742
- # In the first case, it returns True, for the latter - False
743
- return has_not_found
744
-
745
-
746
736
  def setup_hypothesis_database_key(test: Callable, operation: APIOperation) -> None:
747
737
  """Make Hypothesis use separate database entries for every API operation.
748
738
 
@@ -779,7 +769,9 @@ def group_errors(errors: list[Exception]) -> None:
779
769
  serialization_errors = [error for error in errors if isinstance(error, SerializationNotPossible)]
780
770
  if len(serialization_errors) > 1:
781
771
  errors[:] = [error for error in errors if not isinstance(error, SerializationNotPossible)]
782
- media_types = sum((entry.media_types for entry in serialization_errors), [])
772
+ media_types: list[str] = functools.reduce(
773
+ operator.iadd, (entry.media_types for entry in serialization_errors), []
774
+ )
783
775
  errors.append(SerializationNotPossible.from_media_types(*media_types))
784
776
 
785
777
 
@@ -804,6 +796,7 @@ def deduplicate_errors(errors: list[Exception]) -> Generator[Exception, None, No
804
796
  def run_checks(
805
797
  *,
806
798
  case: Case,
799
+ ctx: CheckContext,
807
800
  checks: Iterable[CheckFunction],
808
801
  check_results: list[Check],
809
802
  result: TestResult,
@@ -826,7 +819,7 @@ def run_checks(
826
819
  check_name = check.__name__
827
820
  copied_case = case.partial_deepcopy()
828
821
  try:
829
- skip_check = check(response, copied_case)
822
+ skip_check = check(ctx, response, copied_case)
830
823
  if not skip_check:
831
824
  check_result = result.add_success(check_name, copied_case, response, elapsed_time)
832
825
  check_results.append(check_result)
@@ -912,7 +905,33 @@ def _force_data_generation_method(values: list[DataGenerationMethod], case: Case
912
905
  values[:] = [data_generation_method]
913
906
 
914
907
 
908
+ def cached_test_func(f: Callable) -> Callable:
909
+ def wrapped(*, ctx: RunnerContext, case: Case, **kwargs: Any) -> None:
910
+ if ctx.unique_data:
911
+ cached = ctx.get_cached_outcome(case)
912
+ if isinstance(cached, BaseException):
913
+ raise cached
914
+ elif cached is None:
915
+ return None
916
+ try:
917
+ f(ctx=ctx, case=case, **kwargs)
918
+ except BaseException as exc:
919
+ ctx.cache_outcome(case, exc)
920
+ raise
921
+ else:
922
+ ctx.cache_outcome(case, None)
923
+ else:
924
+ f(ctx=ctx, case=case, **kwargs)
925
+
926
+ wrapped.__name__ = f.__name__
927
+
928
+ return wrapped
929
+
930
+
931
+ @cached_test_func
915
932
  def network_test(
933
+ *,
934
+ ctx: RunnerContext,
916
935
  case: Case,
917
936
  checks: Iterable[CheckFunction],
918
937
  targets: Iterable[Target],
@@ -948,6 +967,8 @@ def network_test(
948
967
  )
949
968
  response = _network_test(case, *args)
950
969
  add_cases(case, response, _network_test, *args)
970
+ elif store_interactions:
971
+ result.store_requests_response(case, None, Status.skip, [], headers=headers, session=session)
951
972
 
952
973
 
953
974
  def _network_test(
@@ -987,13 +1008,18 @@ def _network_test(
987
1008
  check_name, case, None, elapsed, f"Response timed out after {1000 * elapsed:.2f}ms", exc.context, request
988
1009
  )
989
1010
  check_results.append(check_result)
1011
+ if store_interactions:
1012
+ result.store_requests_response(case, None, Status.failure, [check_result], headers=headers, session=session)
990
1013
  raise exc
991
1014
  context = TargetContext(case=case, response=response, response_time=response.elapsed.total_seconds())
992
1015
  run_targets(targets, context)
993
1016
  status = Status.success
1017
+
1018
+ ctx = CheckContext(headers=CaseInsensitiveDict(headers) if headers else None)
994
1019
  try:
995
1020
  run_checks(
996
1021
  case=case,
1022
+ ctx=ctx,
997
1023
  checks=checks,
998
1024
  check_results=check_results,
999
1025
  result=result,
@@ -1008,7 +1034,7 @@ def _network_test(
1008
1034
  if feedback is not None:
1009
1035
  feedback.add_test_case(case, response)
1010
1036
  if store_interactions:
1011
- result.store_requests_response(case, response, status, check_results)
1037
+ result.store_requests_response(case, response, status, check_results, headers=headers, session=session)
1012
1038
  return response
1013
1039
 
1014
1040
 
@@ -1020,7 +1046,9 @@ def get_session(auth: HTTPDigestAuth | RawAuth | None = None) -> Generator[reque
1020
1046
  yield session
1021
1047
 
1022
1048
 
1049
+ @cached_test_func
1023
1050
  def wsgi_test(
1051
+ ctx: RunnerContext,
1024
1052
  case: Case,
1025
1053
  checks: Iterable[CheckFunction],
1026
1054
  targets: Iterable[Target],
@@ -1051,6 +1079,8 @@ def wsgi_test(
1051
1079
  )
1052
1080
  response = _wsgi_test(case, *args)
1053
1081
  add_cases(case, response, _wsgi_test, *args)
1082
+ elif store_interactions:
1083
+ result.store_wsgi_response(case, None, headers, None, Status.skip, [])
1054
1084
 
1055
1085
 
1056
1086
  def _wsgi_test(
@@ -1075,9 +1105,11 @@ def _wsgi_test(
1075
1105
  result.logs.extend(recorded.records)
1076
1106
  status = Status.success
1077
1107
  check_results: list[Check] = []
1108
+ ctx = CheckContext(headers=CaseInsensitiveDict(headers) if headers else None)
1078
1109
  try:
1079
1110
  run_checks(
1080
1111
  case=case,
1112
+ ctx=ctx,
1081
1113
  checks=checks,
1082
1114
  check_results=check_results,
1083
1115
  result=result,
@@ -1096,7 +1128,9 @@ def _wsgi_test(
1096
1128
  return response
1097
1129
 
1098
1130
 
1131
+ @cached_test_func
1099
1132
  def asgi_test(
1133
+ ctx: RunnerContext,
1100
1134
  case: Case,
1101
1135
  checks: Iterable[CheckFunction],
1102
1136
  targets: Iterable[Target],
@@ -1127,6 +1161,8 @@ def asgi_test(
1127
1161
  )
1128
1162
  response = _asgi_test(case, *args)
1129
1163
  add_cases(case, response, _asgi_test, *args)
1164
+ elif store_interactions:
1165
+ result.store_requests_response(case, None, Status.skip, [], headers=headers, session=None)
1130
1166
 
1131
1167
 
1132
1168
  def _asgi_test(
@@ -1147,9 +1183,11 @@ def _asgi_test(
1147
1183
  run_targets(targets, context)
1148
1184
  status = Status.success
1149
1185
  check_results: list[Check] = []
1186
+ ctx = CheckContext(headers=CaseInsensitiveDict(headers) if headers else None)
1150
1187
  try:
1151
1188
  run_checks(
1152
1189
  case=case,
1190
+ ctx=ctx,
1153
1191
  checks=checks,
1154
1192
  check_results=check_results,
1155
1193
  result=result,
@@ -1164,5 +1202,5 @@ def _asgi_test(
1164
1202
  if feedback is not None:
1165
1203
  feedback.add_test_case(case, response)
1166
1204
  if store_interactions:
1167
- result.store_requests_response(case, response, status, check_results)
1205
+ result.store_requests_response(case, response, status, check_results, headers, session=None)
1168
1206
  return response
@@ -1,40 +1,39 @@
1
1
  from __future__ import annotations
2
2
 
3
- import threading
4
3
  from dataclasses import dataclass
5
- from typing import Generator
4
+ from typing import TYPE_CHECKING, Generator
6
5
 
7
- from ...models import TestResultSet
8
6
  from ...transports.auth import get_requests_auth
9
7
  from .. import events
10
8
  from .core import BaseRunner, asgi_test, get_session, network_test, wsgi_test
11
9
 
10
+ if TYPE_CHECKING:
11
+ from .. import events
12
+ from .context import RunnerContext
13
+
12
14
 
13
15
  @dataclass
14
16
  class SingleThreadRunner(BaseRunner):
15
17
  """Fast runner that runs tests sequentially in the main thread."""
16
18
 
17
- def _execute(
18
- self, results: TestResultSet, stop_event: threading.Event
19
- ) -> Generator[events.ExecutionEvent, None, None]:
20
- for event in self._execute_impl(results):
19
+ def _execute(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
20
+ for event in self._execute_impl(ctx):
21
21
  yield event
22
- if stop_event.is_set() or self._should_stop(event):
22
+ if ctx.is_stopped or self._should_stop(event):
23
23
  break
24
24
 
25
- def _execute_impl(self, results: TestResultSet) -> Generator[events.ExecutionEvent, None, None]:
25
+ def _execute_impl(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
26
26
  auth = get_requests_auth(self.auth, self.auth_type)
27
27
  with get_session(auth) as session:
28
28
  yield from self._run_tests(
29
29
  maker=self.schema.get_all_tests,
30
- template=network_test,
30
+ test_func=network_test,
31
31
  settings=self.hypothesis_settings,
32
32
  generation_config=self.generation_config,
33
- seed=self.seed,
34
33
  checks=self.checks,
35
34
  max_response_time=self.max_response_time,
36
35
  targets=self.targets,
37
- results=results,
36
+ ctx=ctx,
38
37
  session=session,
39
38
  headers=self.headers,
40
39
  request_config=self.request_config,
@@ -45,17 +44,16 @@ class SingleThreadRunner(BaseRunner):
45
44
 
46
45
  @dataclass
47
46
  class SingleThreadWSGIRunner(SingleThreadRunner):
48
- def _execute_impl(self, results: TestResultSet) -> Generator[events.ExecutionEvent, None, None]:
47
+ def _execute_impl(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
49
48
  yield from self._run_tests(
50
49
  maker=self.schema.get_all_tests,
51
- template=wsgi_test,
50
+ test_func=wsgi_test,
52
51
  settings=self.hypothesis_settings,
53
52
  generation_config=self.generation_config,
54
- seed=self.seed,
55
53
  checks=self.checks,
56
54
  max_response_time=self.max_response_time,
57
55
  targets=self.targets,
58
- results=results,
56
+ ctx=ctx,
59
57
  auth=self.auth,
60
58
  auth_type=self.auth_type,
61
59
  headers=self.headers,
@@ -66,17 +64,16 @@ class SingleThreadWSGIRunner(SingleThreadRunner):
66
64
 
67
65
  @dataclass
68
66
  class SingleThreadASGIRunner(SingleThreadRunner):
69
- def _execute_impl(self, results: TestResultSet) -> Generator[events.ExecutionEvent, None, None]:
67
+ def _execute_impl(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
70
68
  yield from self._run_tests(
71
69
  maker=self.schema.get_all_tests,
72
- template=asgi_test,
70
+ test_func=asgi_test,
73
71
  settings=self.hypothesis_settings,
74
72
  generation_config=self.generation_config,
75
- seed=self.seed,
76
73
  checks=self.checks,
77
74
  max_response_time=self.max_response_time,
78
75
  targets=self.targets,
79
- results=results,
76
+ ctx=ctx,
80
77
  headers=self.headers,
81
78
  store_interactions=self.store_interactions,
82
79
  dry_run=self.dry_run,