schemathesis 3.25.5__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 +793 -448
  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 +24 -4
  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 +286 -115
  23. schemathesis/cli/output/short.py +25 -6
  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 +323 -213
  61. schemathesis/parameters.py +4 -0
  62. schemathesis/runner/__init__.py +72 -22
  63. schemathesis/runner/events.py +86 -6
  64. schemathesis/runner/impl/context.py +104 -0
  65. schemathesis/runner/impl/core.py +447 -187
  66. schemathesis/runner/impl/solo.py +19 -29
  67. schemathesis/runner/impl/threadpool.py +70 -79
  68. schemathesis/{cli → runner}/probes.py +37 -25
  69. schemathesis/runner/serialization.py +150 -17
  70. schemathesis/sanitization.py +5 -1
  71. schemathesis/schemas.py +170 -102
  72. schemathesis/serializers.py +17 -4
  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 +60 -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 +79 -61
  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 +143 -31
  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 +368 -242
  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.5.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.5.dist-info/METADATA +0 -356
  144. schemathesis-3.25.5.dist-info/RECORD +0 -134
  145. {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
  146. {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
@@ -1,50 +1,42 @@
1
1
  from __future__ import annotations
2
- import threading
2
+
3
3
  from dataclasses import dataclass
4
- from typing import Generator
4
+ from typing import TYPE_CHECKING, Generator
5
5
 
6
- from ...models import TestResultSet
7
- from ...types import RequestCert
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
- request_tls_verify: bool | str = True
18
- request_proxy: str | None = None
19
- request_cert: RequestCert | None = None
20
-
21
- def _execute(
22
- self, results: TestResultSet, stop_event: threading.Event
23
- ) -> Generator[events.ExecutionEvent, None, None]:
24
- 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):
25
21
  yield event
26
- if stop_event.is_set() or self._should_stop(event):
22
+ if ctx.is_stopped or self._should_stop(event):
27
23
  break
28
24
 
29
- def _execute_impl(self, results: TestResultSet) -> Generator[events.ExecutionEvent, None, None]:
25
+ def _execute_impl(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
30
26
  auth = get_requests_auth(self.auth, self.auth_type)
31
27
  with get_session(auth) as session:
32
28
  yield from self._run_tests(
33
29
  maker=self.schema.get_all_tests,
34
- template=network_test,
30
+ test_func=network_test,
35
31
  settings=self.hypothesis_settings,
36
32
  generation_config=self.generation_config,
37
- seed=self.seed,
38
33
  checks=self.checks,
39
34
  max_response_time=self.max_response_time,
40
35
  targets=self.targets,
41
- results=results,
36
+ ctx=ctx,
42
37
  session=session,
43
38
  headers=self.headers,
44
- request_timeout=self.request_timeout,
45
- request_tls_verify=self.request_tls_verify,
46
- request_proxy=self.request_proxy,
47
- request_cert=self.request_cert,
39
+ request_config=self.request_config,
48
40
  store_interactions=self.store_interactions,
49
41
  dry_run=self.dry_run,
50
42
  )
@@ -52,17 +44,16 @@ class SingleThreadRunner(BaseRunner):
52
44
 
53
45
  @dataclass
54
46
  class SingleThreadWSGIRunner(SingleThreadRunner):
55
- def _execute_impl(self, results: TestResultSet) -> Generator[events.ExecutionEvent, None, None]:
47
+ def _execute_impl(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
56
48
  yield from self._run_tests(
57
49
  maker=self.schema.get_all_tests,
58
- template=wsgi_test,
50
+ test_func=wsgi_test,
59
51
  settings=self.hypothesis_settings,
60
52
  generation_config=self.generation_config,
61
- seed=self.seed,
62
53
  checks=self.checks,
63
54
  max_response_time=self.max_response_time,
64
55
  targets=self.targets,
65
- results=results,
56
+ ctx=ctx,
66
57
  auth=self.auth,
67
58
  auth_type=self.auth_type,
68
59
  headers=self.headers,
@@ -73,17 +64,16 @@ class SingleThreadWSGIRunner(SingleThreadRunner):
73
64
 
74
65
  @dataclass
75
66
  class SingleThreadASGIRunner(SingleThreadRunner):
76
- def _execute_impl(self, results: TestResultSet) -> Generator[events.ExecutionEvent, None, None]:
67
+ def _execute_impl(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
77
68
  yield from self._run_tests(
78
69
  maker=self.schema.get_all_tests,
79
- template=asgi_test,
70
+ test_func=asgi_test,
80
71
  settings=self.hypothesis_settings,
81
72
  generation_config=self.generation_config,
82
- seed=self.seed,
83
73
  checks=self.checks,
84
74
  max_response_time=self.max_response_time,
85
75
  targets=self.targets,
86
- results=results,
76
+ ctx=ctx,
87
77
  headers=self.headers,
88
78
  store_interactions=self.store_interactions,
89
79
  dry_run=self.dry_run,
@@ -1,29 +1,37 @@
1
1
  from __future__ import annotations
2
+
2
3
  import ctypes
3
4
  import queue
4
5
  import threading
5
6
  import time
7
+ import warnings
6
8
  from dataclasses import dataclass
7
9
  from queue import Queue
8
- from typing import Any, Callable, Generator, Iterable, cast
10
+ from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, cast
9
11
 
10
- import hypothesis
12
+ from hypothesis.errors import HypothesisWarning
11
13
 
12
14
  from ..._hypothesis import create_test
13
- from ...generation import DataGenerationMethod, GenerationConfig
14
15
  from ...internal.result import Ok
15
- from ...models import CheckFunction, TestResultSet
16
16
  from ...stateful import Feedback, Stateful
17
- from ...targets import Target
18
17
  from ...transports.auth import get_requests_auth
19
- from ...types import RawAuth, RequestCert
20
18
  from ...utils import capture_hypothesis_output
21
19
  from .. import events
22
20
  from .core import BaseRunner, asgi_test, get_session, handle_schema_error, network_test, run_test, wsgi_test
23
21
 
22
+ if TYPE_CHECKING:
23
+ import hypothesis
24
+
25
+ from ...generation import DataGenerationMethod, GenerationConfig
26
+ from ...internal.checks import CheckFunction
27
+ from ...targets import Target
28
+ from ...types import RawAuth
29
+ from .context import RunnerContext
30
+
24
31
 
25
32
  def _run_task(
26
- test_template: Callable,
33
+ *,
34
+ test_func: Callable,
27
35
  tasks_queue: Queue,
28
36
  events_queue: Queue,
29
37
  generator_done: threading.Event,
@@ -32,13 +40,13 @@ def _run_task(
32
40
  data_generation_methods: Iterable[DataGenerationMethod],
33
41
  settings: hypothesis.settings,
34
42
  generation_config: GenerationConfig,
35
- seed: int | None,
36
- results: TestResultSet,
43
+ ctx: RunnerContext,
37
44
  stateful: Stateful | None,
38
45
  stateful_recursion_limit: int,
39
46
  headers: dict[str, Any] | None = None,
40
47
  **kwargs: Any,
41
48
  ) -> None:
49
+ warnings.filterwarnings("ignore", message="The recursion limit will not be reset", category=HypothesisWarning)
42
50
  as_strategy_kwargs = {}
43
51
  if headers is not None:
44
52
  as_strategy_kwargs["headers"] = {key: value for key, value in headers.items() if key.lower() != "user-agent"}
@@ -47,10 +55,10 @@ def _run_task(
47
55
  if recursion_level > stateful_recursion_limit:
48
56
  return
49
57
  for _result in maker(
50
- test_template,
58
+ test_func,
51
59
  settings=settings,
52
60
  generation_config=generation_config,
53
- seed=seed,
61
+ seed=ctx.seed,
54
62
  as_strategy_kwargs=as_strategy_kwargs,
55
63
  ):
56
64
  # `result` is always `Ok` here
@@ -62,7 +70,7 @@ def _run_task(
62
70
  checks,
63
71
  data_generation_methods,
64
72
  targets,
65
- results,
73
+ ctx=ctx,
66
74
  recursion_level=recursion_level,
67
75
  feedback=feedback,
68
76
  headers=headers,
@@ -85,9 +93,9 @@ def _run_task(
85
93
  operation = result.ok()
86
94
  test_function = create_test(
87
95
  operation=operation,
88
- test=test_template,
96
+ test=test_func,
89
97
  settings=settings,
90
- seed=seed,
98
+ seed=ctx.seed,
91
99
  data_generation_methods=list(data_generation_methods),
92
100
  generation_config=generation_config,
93
101
  as_strategy_kwargs=as_strategy_kwargs,
@@ -97,7 +105,7 @@ def _run_task(
97
105
  # `feedback.get_stateful_tests`
98
106
  _run_tests(lambda *_, **__: (items,)) # noqa: B023
99
107
  else:
100
- for event in handle_schema_error(result.err(), results, data_generation_methods, 0):
108
+ for event in handle_schema_error(result.err(), ctx, data_generation_methods, 0):
101
109
  events_queue.put(event)
102
110
 
103
111
 
@@ -113,8 +121,7 @@ def thread_task(
113
121
  auth: RawAuth | None,
114
122
  auth_type: str | None,
115
123
  headers: dict[str, Any] | None,
116
- seed: int | None,
117
- results: TestResultSet,
124
+ ctx: RunnerContext,
118
125
  stateful: Stateful | None,
119
126
  stateful_recursion_limit: int,
120
127
  kwargs: Any,
@@ -126,17 +133,16 @@ def thread_task(
126
133
  prepared_auth = get_requests_auth(auth, auth_type)
127
134
  with get_session(prepared_auth) as session:
128
135
  _run_task(
129
- network_test,
130
- tasks_queue,
131
- events_queue,
132
- generator_done,
133
- checks,
134
- targets,
135
- data_generation_methods,
136
- settings,
137
- generation_config,
138
- seed,
139
- results,
136
+ test_func=network_test,
137
+ tasks_queue=tasks_queue,
138
+ events_queue=events_queue,
139
+ generator_done=generator_done,
140
+ checks=checks,
141
+ targets=targets,
142
+ data_generation_methods=data_generation_methods,
143
+ settings=settings,
144
+ generation_config=generation_config,
145
+ ctx=ctx,
140
146
  stateful=stateful,
141
147
  stateful_recursion_limit=stateful_recursion_limit,
142
148
  session=session,
@@ -154,24 +160,22 @@ def wsgi_thread_task(
154
160
  data_generation_methods: Iterable[DataGenerationMethod],
155
161
  settings: hypothesis.settings,
156
162
  generation_config: GenerationConfig,
157
- seed: int | None,
158
- results: TestResultSet,
163
+ ctx: RunnerContext,
159
164
  stateful: Stateful | None,
160
165
  stateful_recursion_limit: int,
161
166
  kwargs: Any,
162
167
  ) -> None:
163
168
  _run_task(
164
- wsgi_test,
165
- tasks_queue,
166
- events_queue,
167
- generator_done,
168
- checks,
169
- targets,
170
- data_generation_methods,
171
- settings,
172
- generation_config,
173
- seed,
174
- results,
169
+ test_func=wsgi_test,
170
+ tasks_queue=tasks_queue,
171
+ events_queue=events_queue,
172
+ generator_done=generator_done,
173
+ checks=checks,
174
+ targets=targets,
175
+ data_generation_methods=data_generation_methods,
176
+ settings=settings,
177
+ generation_config=generation_config,
178
+ ctx=ctx,
175
179
  stateful=stateful,
176
180
  stateful_recursion_limit=stateful_recursion_limit,
177
181
  **kwargs,
@@ -188,24 +192,22 @@ def asgi_thread_task(
188
192
  settings: hypothesis.settings,
189
193
  generation_config: GenerationConfig,
190
194
  headers: dict[str, Any] | None,
191
- seed: int | None,
192
- results: TestResultSet,
195
+ ctx: RunnerContext,
193
196
  stateful: Stateful | None,
194
197
  stateful_recursion_limit: int,
195
198
  kwargs: Any,
196
199
  ) -> None:
197
200
  _run_task(
198
- asgi_test,
199
- tasks_queue,
200
- events_queue,
201
- generator_done,
202
- checks,
203
- targets,
204
- data_generation_methods,
205
- settings,
206
- generation_config,
207
- seed,
208
- results,
201
+ test_func=asgi_test,
202
+ tasks_queue=tasks_queue,
203
+ events_queue=events_queue,
204
+ generator_done=generator_done,
205
+ checks=checks,
206
+ targets=targets,
207
+ data_generation_methods=data_generation_methods,
208
+ settings=settings,
209
+ generation_config=generation_config,
210
+ ctx=ctx,
209
211
  stateful=stateful,
210
212
  stateful_recursion_limit=stateful_recursion_limit,
211
213
  headers=headers,
@@ -223,13 +225,8 @@ class ThreadPoolRunner(BaseRunner):
223
225
  """Spread different tests among multiple worker threads."""
224
226
 
225
227
  workers_num: int = 2
226
- request_tls_verify: bool | str = True
227
- request_proxy: str | None = None
228
- request_cert: RequestCert | None = None
229
228
 
230
- def _execute(
231
- self, results: TestResultSet, stop_event: threading.Event
232
- ) -> Generator[events.ExecutionEvent, None, None]:
229
+ def _execute(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
233
230
  """All events come from a queue where different workers push their events."""
234
231
  # Instead of generating all tests at once, we do it when there is a free worker to pick it up
235
232
  # This is extremely important for memory consumption when testing large schemas
@@ -237,7 +234,7 @@ class ThreadPoolRunner(BaseRunner):
237
234
  # It would be better to have a separate producer thread and communicate via threading events.
238
235
  # Though it is a bit more complex, so the current solution is suboptimal in terms of resources utilization,
239
236
  # but good enough and easy enough to implement.
240
- tasks_generator = iter(self.schema.get_all_operations())
237
+ tasks_generator = iter(self.schema.get_all_operations(generation_config=self.generation_config))
241
238
  generator_done = threading.Event()
242
239
  tasks_queue: Queue = Queue()
243
240
  # Add at least `workers_num` tasks first, so all workers are busy
@@ -250,7 +247,7 @@ class ThreadPoolRunner(BaseRunner):
250
247
  break
251
248
  # Events are pushed by workers via a separate queue
252
249
  events_queue: Queue = Queue()
253
- workers = self._init_workers(tasks_queue, events_queue, results, generator_done)
250
+ workers = self._init_workers(tasks_queue, events_queue, ctx, generator_done)
254
251
 
255
252
  def stop_workers() -> None:
256
253
  for worker in workers:
@@ -269,12 +266,12 @@ class ThreadPoolRunner(BaseRunner):
269
266
  is_finished = all(not worker.is_alive() for worker in workers)
270
267
  while not events_queue.empty():
271
268
  event = events_queue.get()
272
- if stop_event.is_set() or isinstance(event, events.Interrupted) or self._should_stop(event):
269
+ if ctx.is_stopped or isinstance(event, events.Interrupted) or self._should_stop(event):
273
270
  # We could still have events in the queue, but ignore them to keep the logic simple
274
271
  # for now, could be improved in the future to show more info in such corner cases
275
272
  stop_workers()
276
273
  is_finished = True
277
- if stop_event.is_set():
274
+ if ctx.is_stopped:
278
275
  # Discard the event. The invariant is: the next event after `stream.stop()` is `Finished`
279
276
  break
280
277
  yield event
@@ -291,13 +288,13 @@ class ThreadPoolRunner(BaseRunner):
291
288
  yield events.Interrupted()
292
289
 
293
290
  def _init_workers(
294
- self, tasks_queue: Queue, events_queue: Queue, results: TestResultSet, generator_done: threading.Event
291
+ self, tasks_queue: Queue, events_queue: Queue, ctx: RunnerContext, generator_done: threading.Event
295
292
  ) -> list[threading.Thread]:
296
293
  """Initialize & start workers that will execute tests."""
297
294
  workers = [
298
295
  threading.Thread(
299
296
  target=self._get_task(),
300
- kwargs=self._get_worker_kwargs(tasks_queue, events_queue, results, generator_done),
297
+ kwargs=self._get_worker_kwargs(tasks_queue, events_queue, ctx, generator_done),
301
298
  name=f"schemathesis_{num}",
302
299
  )
303
300
  for num in range(self.workers_num)
@@ -310,7 +307,7 @@ class ThreadPoolRunner(BaseRunner):
310
307
  return thread_task
311
308
 
312
309
  def _get_worker_kwargs(
313
- self, tasks_queue: Queue, events_queue: Queue, results: TestResultSet, generator_done: threading.Event
310
+ self, tasks_queue: Queue, events_queue: Queue, ctx: RunnerContext, generator_done: threading.Event
314
311
  ) -> dict[str, Any]:
315
312
  return {
316
313
  "tasks_queue": tasks_queue,
@@ -323,16 +320,12 @@ class ThreadPoolRunner(BaseRunner):
323
320
  "auth": self.auth,
324
321
  "auth_type": self.auth_type,
325
322
  "headers": self.headers,
326
- "seed": self.seed,
327
- "results": results,
323
+ "ctx": ctx,
328
324
  "stateful": self.stateful,
329
325
  "stateful_recursion_limit": self.stateful_recursion_limit,
330
326
  "data_generation_methods": self.schema.data_generation_methods,
331
327
  "kwargs": {
332
- "request_timeout": self.request_timeout,
333
- "request_tls_verify": self.request_tls_verify,
334
- "request_proxy": self.request_proxy,
335
- "request_cert": self.request_cert,
328
+ "request_config": self.request_config,
336
329
  "store_interactions": self.store_interactions,
337
330
  "max_response_time": self.max_response_time,
338
331
  "dry_run": self.dry_run,
@@ -345,7 +338,7 @@ class ThreadPoolWSGIRunner(ThreadPoolRunner):
345
338
  return wsgi_thread_task
346
339
 
347
340
  def _get_worker_kwargs(
348
- self, tasks_queue: Queue, events_queue: Queue, results: TestResultSet, generator_done: threading.Event
341
+ self, tasks_queue: Queue, events_queue: Queue, ctx: RunnerContext, generator_done: threading.Event
349
342
  ) -> dict[str, Any]:
350
343
  return {
351
344
  "tasks_queue": tasks_queue,
@@ -355,8 +348,7 @@ class ThreadPoolWSGIRunner(ThreadPoolRunner):
355
348
  "targets": self.targets,
356
349
  "settings": self.hypothesis_settings,
357
350
  "generation_config": self.generation_config,
358
- "seed": self.seed,
359
- "results": results,
351
+ "ctx": ctx,
360
352
  "stateful": self.stateful,
361
353
  "stateful_recursion_limit": self.stateful_recursion_limit,
362
354
  "data_generation_methods": self.schema.data_generation_methods,
@@ -376,7 +368,7 @@ class ThreadPoolASGIRunner(ThreadPoolRunner):
376
368
  return asgi_thread_task
377
369
 
378
370
  def _get_worker_kwargs(
379
- self, tasks_queue: Queue, events_queue: Queue, results: TestResultSet, generator_done: threading.Event
371
+ self, tasks_queue: Queue, events_queue: Queue, ctx: RunnerContext, generator_done: threading.Event
380
372
  ) -> dict[str, Any]:
381
373
  return {
382
374
  "tasks_queue": tasks_queue,
@@ -387,8 +379,7 @@ class ThreadPoolASGIRunner(ThreadPoolRunner):
387
379
  "settings": self.hypothesis_settings,
388
380
  "generation_config": self.generation_config,
389
381
  "headers": self.headers,
390
- "seed": self.seed,
391
- "results": results,
382
+ "ctx": ctx,
392
383
  "stateful": self.stateful,
393
384
  "stateful_recursion_limit": self.stateful_recursion_limit,
394
385
  "data_generation_methods": self.schema.data_generation_methods,
@@ -5,29 +5,39 @@ the application supports certain inputs. This is done to avoid false positives i
5
5
  For example, certail web servers do not support NULL bytes in headers, in such cases, the generated test case
6
6
  will not reach the tested application at all.
7
7
  """
8
+
8
9
  from __future__ import annotations
9
10
 
10
11
  import enum
11
12
  import warnings
12
- from dataclasses import asdict, dataclass
13
+ from dataclasses import asdict, dataclass, field
13
14
  from typing import TYPE_CHECKING, Any
14
15
 
15
16
  from ..constants import USER_AGENT
16
17
  from ..exceptions import format_exception
17
18
  from ..models import Request, Response
18
19
  from ..sanitization import sanitize_request, sanitize_response
20
+ from ..transports import RequestConfig
19
21
  from ..transports.auth import get_requests_auth
20
22
 
21
23
  if TYPE_CHECKING:
22
24
  import requests
23
25
 
24
26
  from ..schemas import BaseSchema
25
- from . import LoaderConfig
26
27
 
27
28
 
28
29
  HEADER_NAME = "X-Schemathesis-Probe"
29
30
 
30
31
 
32
+ @dataclass
33
+ class ProbeConfig:
34
+ base_url: str | None = None
35
+ request: RequestConfig = field(default_factory=RequestConfig)
36
+ auth: tuple[str, str] | None = None
37
+ auth_type: str | None = None
38
+ headers: dict[str, str] | None = None
39
+
40
+
31
41
  @dataclass
32
42
  class Probe:
33
43
  """A request to determine the capabilities of the application under test."""
@@ -35,15 +45,15 @@ class Probe:
35
45
  name: str
36
46
 
37
47
  def prepare_request(
38
- self, session: requests.Session, request: requests.Request, schema: BaseSchema, config: LoaderConfig
48
+ self, session: requests.Session, request: requests.Request, schema: BaseSchema, config: ProbeConfig
39
49
  ) -> requests.PreparedRequest:
40
50
  raise NotImplementedError
41
51
 
42
- def analyze_response(self, response: requests.Response) -> ProbeResultType:
52
+ def analyze_response(self, response: requests.Response) -> ProbeOutcome:
43
53
  raise NotImplementedError
44
54
 
45
55
 
46
- class ProbeResultType(str, enum.Enum):
56
+ class ProbeOutcome(str, enum.Enum):
47
57
  # Capability is supported
48
58
  SUCCESS = "success"
49
59
  # Capability is not supported
@@ -55,18 +65,16 @@ class ProbeResultType(str, enum.Enum):
55
65
 
56
66
 
57
67
  @dataclass
58
- class ProbeResult:
59
- """Result of a probe."""
60
-
68
+ class ProbeRun:
61
69
  probe: Probe
62
- type: ProbeResultType
70
+ outcome: ProbeOutcome
63
71
  request: requests.PreparedRequest | None = None
64
72
  response: requests.Response | None = None
65
73
  error: requests.RequestException | None = None
66
74
 
67
75
  @property
68
76
  def is_failure(self) -> bool:
69
- return self.type == ProbeResultType.FAILURE
77
+ return self.outcome == ProbeOutcome.FAILURE
70
78
 
71
79
  def serialize(self) -> dict[str, Any]:
72
80
  """Serialize probe results so it can be sent over the network."""
@@ -87,7 +95,7 @@ class ProbeResult:
87
95
  error = None
88
96
  return {
89
97
  "name": self.probe.name,
90
- "type": self.type.value,
98
+ "outcome": self.outcome.value,
91
99
  "request": request,
92
100
  "response": response,
93
101
  "error": error,
@@ -101,25 +109,25 @@ class NullByteInHeader(Probe):
101
109
  name: str = "NULL_BYTE_IN_HEADER"
102
110
 
103
111
  def prepare_request(
104
- self, session: requests.Session, request: requests.Request, schema: BaseSchema, config: LoaderConfig
112
+ self, session: requests.Session, request: requests.Request, schema: BaseSchema, config: ProbeConfig
105
113
  ) -> requests.PreparedRequest:
106
114
  request.method = "GET"
107
115
  request.url = config.base_url or schema.get_base_url()
108
116
  request.headers = {"X-Schemathesis-Probe-Null": "\x00"}
109
117
  return session.prepare_request(request)
110
118
 
111
- def analyze_response(self, response: requests.Response) -> ProbeResultType:
119
+ def analyze_response(self, response: requests.Response) -> ProbeOutcome:
112
120
  if response.status_code == 400:
113
- return ProbeResultType.FAILURE
114
- return ProbeResultType.SUCCESS
121
+ return ProbeOutcome.FAILURE
122
+ return ProbeOutcome.SUCCESS
115
123
 
116
124
 
117
125
  PROBES = (NullByteInHeader,)
118
126
 
119
127
 
120
- def send(probe: Probe, session: requests.Session, schema: BaseSchema, config: LoaderConfig) -> ProbeResult:
128
+ def send(probe: Probe, session: requests.Session, schema: BaseSchema, config: ProbeConfig) -> ProbeRun:
121
129
  """Send the probe to the application."""
122
- from requests import Request, RequestException, PreparedRequest
130
+ from requests import PreparedRequest, Request, RequestException
123
131
  from requests.exceptions import MissingSchema
124
132
  from urllib3.exceptions import InsecureRequestWarning
125
133
 
@@ -127,28 +135,32 @@ def send(probe: Probe, session: requests.Session, schema: BaseSchema, config: Lo
127
135
  request = probe.prepare_request(session, Request(), schema, config)
128
136
  request.headers[HEADER_NAME] = probe.name
129
137
  request.headers["User-Agent"] = USER_AGENT
138
+ kwargs: dict[str, Any] = {"timeout": config.request.prepared_timeout or 2}
139
+ if config.request.proxy is not None:
140
+ kwargs["proxies"] = {"all": config.request.proxy}
130
141
  with warnings.catch_warnings():
131
142
  warnings.simplefilter("ignore", InsecureRequestWarning)
132
- response = session.send(request)
143
+ response = session.send(request, **kwargs)
133
144
  except MissingSchema:
134
145
  # In-process ASGI/WSGI testing will have local URLs and requires extra handling
135
146
  # which is not currently implemented
136
- return ProbeResult(probe, ProbeResultType.SKIP, None, None, None)
147
+ return ProbeRun(probe, ProbeOutcome.SKIP, None, None, None)
137
148
  except RequestException as exc:
138
149
  req = exc.request if isinstance(exc.request, PreparedRequest) else None
139
- return ProbeResult(probe, ProbeResultType.ERROR, req, None, exc)
150
+ return ProbeRun(probe, ProbeOutcome.ERROR, req, None, exc)
140
151
  result_type = probe.analyze_response(response)
141
- return ProbeResult(probe, result_type, request, response)
152
+ return ProbeRun(probe, result_type, request, response)
142
153
 
143
154
 
144
- def run(schema: BaseSchema, config: LoaderConfig) -> list[ProbeResult]:
155
+ def run(schema: BaseSchema, config: ProbeConfig) -> list[ProbeRun]:
145
156
  """Run all probes against the given schema."""
146
157
  from requests import Session
147
158
 
148
159
  session = Session()
149
- session.verify = config.request_tls_verify
150
- if config.request_cert is not None:
151
- session.cert = config.request_cert
160
+ session.headers.update(config.headers or {})
161
+ session.verify = config.request.tls_verify
162
+ if config.request.cert is not None:
163
+ session.cert = config.request.cert
152
164
  if config.auth is not None:
153
165
  session.auth = get_requests_auth(config.auth, config.auth_type)
154
166