schemathesis 3.39.7__py3-none-any.whl → 4.0.0a2__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 (229) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +26 -68
  3. schemathesis/checks.py +130 -60
  4. schemathesis/cli/__init__.py +5 -2105
  5. schemathesis/cli/commands/__init__.py +37 -0
  6. schemathesis/cli/commands/run/__init__.py +662 -0
  7. schemathesis/cli/commands/run/checks.py +80 -0
  8. schemathesis/cli/commands/run/context.py +117 -0
  9. schemathesis/cli/commands/run/events.py +30 -0
  10. schemathesis/cli/commands/run/executor.py +141 -0
  11. schemathesis/cli/commands/run/filters.py +202 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/{cassettes.py → commands/run/handlers/cassettes.py} +178 -247
  15. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1368 -0
  17. schemathesis/cli/commands/run/hypothesis.py +105 -0
  18. schemathesis/cli/commands/run/loaders.py +129 -0
  19. schemathesis/cli/{callbacks.py → commands/run/validation.py} +59 -175
  20. schemathesis/cli/constants.py +5 -58
  21. schemathesis/cli/core.py +17 -0
  22. schemathesis/cli/ext/fs.py +14 -0
  23. schemathesis/cli/ext/groups.py +55 -0
  24. schemathesis/cli/{options.py → ext/options.py} +37 -16
  25. schemathesis/cli/hooks.py +36 -0
  26. schemathesis/contrib/__init__.py +1 -3
  27. schemathesis/contrib/openapi/__init__.py +1 -3
  28. schemathesis/contrib/openapi/fill_missing_examples.py +3 -7
  29. schemathesis/core/__init__.py +58 -0
  30. schemathesis/core/compat.py +25 -0
  31. schemathesis/core/control.py +2 -0
  32. schemathesis/core/curl.py +58 -0
  33. schemathesis/core/deserialization.py +65 -0
  34. schemathesis/core/errors.py +370 -0
  35. schemathesis/core/failures.py +315 -0
  36. schemathesis/core/fs.py +19 -0
  37. schemathesis/core/loaders.py +104 -0
  38. schemathesis/core/marks.py +66 -0
  39. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  40. schemathesis/{internal/output.py → core/output/__init__.py} +1 -0
  41. schemathesis/core/output/sanitization.py +197 -0
  42. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  43. schemathesis/core/registries.py +31 -0
  44. schemathesis/core/transforms.py +113 -0
  45. schemathesis/core/transport.py +108 -0
  46. schemathesis/core/validation.py +38 -0
  47. schemathesis/core/version.py +7 -0
  48. schemathesis/engine/__init__.py +30 -0
  49. schemathesis/engine/config.py +59 -0
  50. schemathesis/engine/context.py +119 -0
  51. schemathesis/engine/control.py +36 -0
  52. schemathesis/engine/core.py +157 -0
  53. schemathesis/engine/errors.py +394 -0
  54. schemathesis/engine/events.py +243 -0
  55. schemathesis/engine/phases/__init__.py +66 -0
  56. schemathesis/{runner → engine/phases}/probes.py +49 -68
  57. schemathesis/engine/phases/stateful/__init__.py +66 -0
  58. schemathesis/engine/phases/stateful/_executor.py +301 -0
  59. schemathesis/engine/phases/stateful/context.py +85 -0
  60. schemathesis/engine/phases/unit/__init__.py +175 -0
  61. schemathesis/engine/phases/unit/_executor.py +322 -0
  62. schemathesis/engine/phases/unit/_pool.py +74 -0
  63. schemathesis/engine/recorder.py +246 -0
  64. schemathesis/errors.py +31 -0
  65. schemathesis/experimental/__init__.py +9 -40
  66. schemathesis/filters.py +7 -95
  67. schemathesis/generation/__init__.py +3 -3
  68. schemathesis/generation/case.py +190 -0
  69. schemathesis/generation/coverage.py +22 -22
  70. schemathesis/{_patches.py → generation/hypothesis/__init__.py} +15 -6
  71. schemathesis/generation/hypothesis/builder.py +585 -0
  72. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  73. schemathesis/generation/hypothesis/given.py +66 -0
  74. schemathesis/generation/hypothesis/reporting.py +14 -0
  75. schemathesis/generation/hypothesis/strategies.py +16 -0
  76. schemathesis/generation/meta.py +115 -0
  77. schemathesis/generation/modes.py +28 -0
  78. schemathesis/generation/overrides.py +96 -0
  79. schemathesis/generation/stateful/__init__.py +20 -0
  80. schemathesis/{stateful → generation/stateful}/state_machine.py +84 -109
  81. schemathesis/generation/targets.py +69 -0
  82. schemathesis/graphql/__init__.py +15 -0
  83. schemathesis/graphql/checks.py +109 -0
  84. schemathesis/graphql/loaders.py +131 -0
  85. schemathesis/hooks.py +17 -62
  86. schemathesis/openapi/__init__.py +13 -0
  87. schemathesis/openapi/checks.py +387 -0
  88. schemathesis/openapi/generation/__init__.py +0 -0
  89. schemathesis/openapi/generation/filters.py +63 -0
  90. schemathesis/openapi/loaders.py +178 -0
  91. schemathesis/pytest/__init__.py +5 -0
  92. schemathesis/pytest/control_flow.py +7 -0
  93. schemathesis/pytest/lazy.py +273 -0
  94. schemathesis/pytest/loaders.py +12 -0
  95. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +94 -107
  96. schemathesis/python/__init__.py +0 -0
  97. schemathesis/python/asgi.py +12 -0
  98. schemathesis/python/wsgi.py +12 -0
  99. schemathesis/schemas.py +456 -228
  100. schemathesis/specs/graphql/__init__.py +0 -1
  101. schemathesis/specs/graphql/_cache.py +1 -2
  102. schemathesis/specs/graphql/scalars.py +5 -3
  103. schemathesis/specs/graphql/schemas.py +122 -123
  104. schemathesis/specs/graphql/validation.py +11 -17
  105. schemathesis/specs/openapi/__init__.py +6 -1
  106. schemathesis/specs/openapi/_cache.py +1 -2
  107. schemathesis/specs/openapi/_hypothesis.py +97 -134
  108. schemathesis/specs/openapi/checks.py +238 -219
  109. schemathesis/specs/openapi/converter.py +4 -4
  110. schemathesis/specs/openapi/definitions.py +1 -1
  111. schemathesis/specs/openapi/examples.py +22 -20
  112. schemathesis/specs/openapi/expressions/__init__.py +11 -15
  113. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  114. schemathesis/specs/openapi/expressions/nodes.py +33 -32
  115. schemathesis/specs/openapi/formats.py +3 -2
  116. schemathesis/specs/openapi/links.py +123 -299
  117. schemathesis/specs/openapi/media_types.py +10 -12
  118. schemathesis/specs/openapi/negative/__init__.py +2 -1
  119. schemathesis/specs/openapi/negative/mutations.py +3 -2
  120. schemathesis/specs/openapi/parameters.py +8 -6
  121. schemathesis/specs/openapi/patterns.py +1 -1
  122. schemathesis/specs/openapi/references.py +11 -51
  123. schemathesis/specs/openapi/schemas.py +177 -191
  124. schemathesis/specs/openapi/security.py +1 -1
  125. schemathesis/specs/openapi/serialization.py +10 -6
  126. schemathesis/specs/openapi/stateful/__init__.py +97 -91
  127. schemathesis/transport/__init__.py +104 -0
  128. schemathesis/transport/asgi.py +26 -0
  129. schemathesis/transport/prepare.py +99 -0
  130. schemathesis/transport/requests.py +221 -0
  131. schemathesis/{_xml.py → transport/serialization.py} +69 -7
  132. schemathesis/transport/wsgi.py +165 -0
  133. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +18 -14
  134. schemathesis-4.0.0a2.dist-info/RECORD +151 -0
  135. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +1 -1
  136. schemathesis/_compat.py +0 -74
  137. schemathesis/_dependency_versions.py +0 -19
  138. schemathesis/_hypothesis.py +0 -559
  139. schemathesis/_override.py +0 -50
  140. schemathesis/_rate_limiter.py +0 -7
  141. schemathesis/cli/context.py +0 -75
  142. schemathesis/cli/debug.py +0 -27
  143. schemathesis/cli/handlers.py +0 -19
  144. schemathesis/cli/junitxml.py +0 -124
  145. schemathesis/cli/output/__init__.py +0 -1
  146. schemathesis/cli/output/default.py +0 -936
  147. schemathesis/cli/output/short.py +0 -59
  148. schemathesis/cli/reporting.py +0 -79
  149. schemathesis/cli/sanitization.py +0 -26
  150. schemathesis/code_samples.py +0 -151
  151. schemathesis/constants.py +0 -56
  152. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  153. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  154. schemathesis/contrib/unique_data.py +0 -41
  155. schemathesis/exceptions.py +0 -571
  156. schemathesis/extra/_aiohttp.py +0 -28
  157. schemathesis/extra/_flask.py +0 -13
  158. schemathesis/extra/_server.py +0 -18
  159. schemathesis/failures.py +0 -277
  160. schemathesis/fixups/__init__.py +0 -37
  161. schemathesis/fixups/fast_api.py +0 -41
  162. schemathesis/fixups/utf8_bom.py +0 -28
  163. schemathesis/generation/_methods.py +0 -44
  164. schemathesis/graphql.py +0 -3
  165. schemathesis/internal/__init__.py +0 -7
  166. schemathesis/internal/checks.py +0 -84
  167. schemathesis/internal/copy.py +0 -32
  168. schemathesis/internal/datetime.py +0 -5
  169. schemathesis/internal/deprecation.py +0 -38
  170. schemathesis/internal/diff.py +0 -15
  171. schemathesis/internal/extensions.py +0 -27
  172. schemathesis/internal/jsonschema.py +0 -36
  173. schemathesis/internal/transformation.py +0 -26
  174. schemathesis/internal/validation.py +0 -34
  175. schemathesis/lazy.py +0 -474
  176. schemathesis/loaders.py +0 -122
  177. schemathesis/models.py +0 -1341
  178. schemathesis/parameters.py +0 -90
  179. schemathesis/runner/__init__.py +0 -605
  180. schemathesis/runner/events.py +0 -389
  181. schemathesis/runner/impl/__init__.py +0 -3
  182. schemathesis/runner/impl/context.py +0 -104
  183. schemathesis/runner/impl/core.py +0 -1246
  184. schemathesis/runner/impl/solo.py +0 -80
  185. schemathesis/runner/impl/threadpool.py +0 -391
  186. schemathesis/runner/serialization.py +0 -544
  187. schemathesis/sanitization.py +0 -252
  188. schemathesis/serializers.py +0 -328
  189. schemathesis/service/__init__.py +0 -18
  190. schemathesis/service/auth.py +0 -11
  191. schemathesis/service/ci.py +0 -202
  192. schemathesis/service/client.py +0 -133
  193. schemathesis/service/constants.py +0 -38
  194. schemathesis/service/events.py +0 -61
  195. schemathesis/service/extensions.py +0 -224
  196. schemathesis/service/hosts.py +0 -111
  197. schemathesis/service/metadata.py +0 -71
  198. schemathesis/service/models.py +0 -258
  199. schemathesis/service/report.py +0 -255
  200. schemathesis/service/serialization.py +0 -173
  201. schemathesis/service/usage.py +0 -66
  202. schemathesis/specs/graphql/loaders.py +0 -364
  203. schemathesis/specs/openapi/expressions/context.py +0 -16
  204. schemathesis/specs/openapi/loaders.py +0 -708
  205. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  206. schemathesis/specs/openapi/stateful/types.py +0 -14
  207. schemathesis/specs/openapi/validation.py +0 -26
  208. schemathesis/stateful/__init__.py +0 -147
  209. schemathesis/stateful/config.py +0 -97
  210. schemathesis/stateful/context.py +0 -135
  211. schemathesis/stateful/events.py +0 -274
  212. schemathesis/stateful/runner.py +0 -309
  213. schemathesis/stateful/sink.py +0 -68
  214. schemathesis/stateful/statistic.py +0 -22
  215. schemathesis/stateful/validation.py +0 -100
  216. schemathesis/targets.py +0 -77
  217. schemathesis/transports/__init__.py +0 -359
  218. schemathesis/transports/asgi.py +0 -7
  219. schemathesis/transports/auth.py +0 -38
  220. schemathesis/transports/headers.py +0 -36
  221. schemathesis/transports/responses.py +0 -57
  222. schemathesis/types.py +0 -44
  223. schemathesis/utils.py +0 -164
  224. schemathesis-3.39.7.dist-info/RECORD +0 -160
  225. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  226. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  227. /schemathesis/{internal → core}/result.py +0 -0
  228. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
  229. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -1,80 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from typing import TYPE_CHECKING, Generator
5
-
6
- from ...transports.auth import get_requests_auth
7
- from .. import events
8
- from .core import BaseRunner, asgi_test, get_session, network_test, wsgi_test
9
-
10
- if TYPE_CHECKING:
11
- from .. import events
12
- from .context import RunnerContext
13
-
14
-
15
- @dataclass
16
- class SingleThreadRunner(BaseRunner):
17
- """Fast runner that runs tests sequentially in the main thread."""
18
-
19
- def _execute(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
20
- for event in self._execute_impl(ctx):
21
- yield event
22
- if ctx.is_stopped or self._should_stop(event):
23
- break
24
-
25
- def _execute_impl(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
26
- auth = get_requests_auth(self.auth, self.auth_type)
27
- with get_session(auth) as session:
28
- yield from self._run_tests(
29
- maker=self.schema.get_all_tests,
30
- test_func=network_test,
31
- settings=self.hypothesis_settings,
32
- generation_config=self.generation_config,
33
- checks=self.checks,
34
- max_response_time=self.max_response_time,
35
- targets=self.targets,
36
- ctx=ctx,
37
- session=session,
38
- headers=self.headers,
39
- request_config=self.request_config,
40
- store_interactions=self.store_interactions,
41
- dry_run=self.dry_run,
42
- )
43
-
44
-
45
- @dataclass
46
- class SingleThreadWSGIRunner(SingleThreadRunner):
47
- def _execute_impl(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
48
- yield from self._run_tests(
49
- maker=self.schema.get_all_tests,
50
- test_func=wsgi_test,
51
- settings=self.hypothesis_settings,
52
- generation_config=self.generation_config,
53
- checks=self.checks,
54
- max_response_time=self.max_response_time,
55
- targets=self.targets,
56
- ctx=ctx,
57
- auth=self.auth,
58
- auth_type=self.auth_type,
59
- headers=self.headers,
60
- store_interactions=self.store_interactions,
61
- dry_run=self.dry_run,
62
- )
63
-
64
-
65
- @dataclass
66
- class SingleThreadASGIRunner(SingleThreadRunner):
67
- def _execute_impl(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
68
- yield from self._run_tests(
69
- maker=self.schema.get_all_tests,
70
- test_func=asgi_test,
71
- settings=self.hypothesis_settings,
72
- generation_config=self.generation_config,
73
- checks=self.checks,
74
- max_response_time=self.max_response_time,
75
- targets=self.targets,
76
- ctx=ctx,
77
- headers=self.headers,
78
- store_interactions=self.store_interactions,
79
- dry_run=self.dry_run,
80
- )
@@ -1,391 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import ctypes
4
- import queue
5
- import threading
6
- import time
7
- import warnings
8
- from dataclasses import dataclass
9
- from queue import Queue
10
- from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, cast
11
-
12
- from hypothesis.errors import HypothesisWarning
13
-
14
- from ..._hypothesis import create_test
15
- from ...internal.result import Ok
16
- from ...stateful import Feedback, Stateful
17
- from ...transports.auth import get_requests_auth
18
- from ...utils import capture_hypothesis_output
19
- from .. import events
20
- from .core import BaseRunner, asgi_test, get_session, handle_schema_error, network_test, run_test, wsgi_test
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
-
31
-
32
- def _run_task(
33
- *,
34
- test_func: Callable,
35
- tasks_queue: Queue,
36
- events_queue: Queue,
37
- generator_done: threading.Event,
38
- checks: Iterable[CheckFunction],
39
- targets: Iterable[Target],
40
- data_generation_methods: Iterable[DataGenerationMethod],
41
- settings: hypothesis.settings,
42
- generation_config: GenerationConfig,
43
- ctx: RunnerContext,
44
- stateful: Stateful | None,
45
- stateful_recursion_limit: int,
46
- headers: dict[str, Any] | None = None,
47
- **kwargs: Any,
48
- ) -> None:
49
- warnings.filterwarnings("ignore", message="The recursion limit will not be reset", category=HypothesisWarning)
50
- as_strategy_kwargs = {}
51
- if headers is not None:
52
- as_strategy_kwargs["headers"] = {key: value for key, value in headers.items() if key.lower() != "user-agent"}
53
-
54
- def _run_tests(maker: Callable, recursion_level: int = 0) -> None:
55
- if recursion_level > stateful_recursion_limit:
56
- return
57
- for _result in maker(
58
- test_func,
59
- settings=settings,
60
- generation_config=generation_config,
61
- seed=ctx.seed,
62
- as_strategy_kwargs=as_strategy_kwargs,
63
- ):
64
- # `result` is always `Ok` here
65
- _operation, test = _result.ok()
66
- feedback = Feedback(stateful, _operation)
67
- for _event in run_test(
68
- _operation,
69
- test,
70
- checks,
71
- data_generation_methods,
72
- targets,
73
- ctx=ctx,
74
- recursion_level=recursion_level,
75
- feedback=feedback,
76
- headers=headers,
77
- **kwargs,
78
- ):
79
- events_queue.put(_event)
80
- _run_tests(feedback.get_stateful_tests, recursion_level + 1)
81
-
82
- with capture_hypothesis_output():
83
- while True:
84
- try:
85
- result = tasks_queue.get(timeout=0.001)
86
- except queue.Empty:
87
- # The queue is empty & there will be no more tasks
88
- if generator_done.is_set():
89
- break
90
- # If there is a possibility for new tasks - try again
91
- continue
92
- if isinstance(result, Ok):
93
- operation = result.ok()
94
- test_function = create_test(
95
- operation=operation,
96
- test=test_func,
97
- settings=settings,
98
- seed=ctx.seed,
99
- data_generation_methods=list(data_generation_methods),
100
- generation_config=generation_config,
101
- as_strategy_kwargs=as_strategy_kwargs,
102
- )
103
- items = Ok((operation, test_function))
104
- # This lambda ignores the input arguments to support the same interface for
105
- # `feedback.get_stateful_tests`
106
- _run_tests(lambda *_, **__: (items,)) # noqa: B023
107
- else:
108
- for event in handle_schema_error(result.err(), ctx, data_generation_methods, 0):
109
- events_queue.put(event)
110
-
111
-
112
- def thread_task(
113
- tasks_queue: Queue,
114
- events_queue: Queue,
115
- generator_done: threading.Event,
116
- checks: Iterable[CheckFunction],
117
- targets: Iterable[Target],
118
- data_generation_methods: Iterable[DataGenerationMethod],
119
- settings: hypothesis.settings,
120
- generation_config: GenerationConfig,
121
- auth: RawAuth | None,
122
- auth_type: str | None,
123
- headers: dict[str, Any] | None,
124
- ctx: RunnerContext,
125
- stateful: Stateful | None,
126
- stateful_recursion_limit: int,
127
- kwargs: Any,
128
- ) -> None:
129
- """A single task, that threads do.
130
-
131
- Pretty similar to the default one-thread flow, but includes communication with the main thread via the events queue.
132
- """
133
- prepared_auth = get_requests_auth(auth, auth_type)
134
- with get_session(prepared_auth) as session:
135
- _run_task(
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,
146
- stateful=stateful,
147
- stateful_recursion_limit=stateful_recursion_limit,
148
- session=session,
149
- headers=headers,
150
- **kwargs,
151
- )
152
-
153
-
154
- def wsgi_thread_task(
155
- tasks_queue: Queue,
156
- events_queue: Queue,
157
- generator_done: threading.Event,
158
- checks: Iterable[CheckFunction],
159
- targets: Iterable[Target],
160
- data_generation_methods: Iterable[DataGenerationMethod],
161
- settings: hypothesis.settings,
162
- generation_config: GenerationConfig,
163
- ctx: RunnerContext,
164
- stateful: Stateful | None,
165
- stateful_recursion_limit: int,
166
- kwargs: Any,
167
- ) -> None:
168
- _run_task(
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,
179
- stateful=stateful,
180
- stateful_recursion_limit=stateful_recursion_limit,
181
- **kwargs,
182
- )
183
-
184
-
185
- def asgi_thread_task(
186
- tasks_queue: Queue,
187
- events_queue: Queue,
188
- generator_done: threading.Event,
189
- checks: Iterable[CheckFunction],
190
- targets: Iterable[Target],
191
- data_generation_methods: Iterable[DataGenerationMethod],
192
- settings: hypothesis.settings,
193
- generation_config: GenerationConfig,
194
- headers: dict[str, Any] | None,
195
- ctx: RunnerContext,
196
- stateful: Stateful | None,
197
- stateful_recursion_limit: int,
198
- kwargs: Any,
199
- ) -> None:
200
- _run_task(
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,
211
- stateful=stateful,
212
- stateful_recursion_limit=stateful_recursion_limit,
213
- headers=headers,
214
- **kwargs,
215
- )
216
-
217
-
218
- def stop_worker(thread_id: int) -> None:
219
- """Raise an error in a thread, so it is possible to asynchronously stop thread execution."""
220
- ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(thread_id), ctypes.py_object(SystemExit))
221
-
222
-
223
- @dataclass
224
- class ThreadPoolRunner(BaseRunner):
225
- """Spread different tests among multiple worker threads."""
226
-
227
- workers_num: int = 2
228
-
229
- def _execute(self, ctx: RunnerContext) -> Generator[events.ExecutionEvent, None, None]:
230
- """All events come from a queue where different workers push their events."""
231
- # Instead of generating all tests at once, we do it when there is a free worker to pick it up
232
- # This is extremely important for memory consumption when testing large schemas
233
- # IMPLEMENTATION NOTE:
234
- # It would be better to have a separate producer thread and communicate via threading events.
235
- # Though it is a bit more complex, so the current solution is suboptimal in terms of resources utilization,
236
- # but good enough and easy enough to implement.
237
- tasks_generator = iter(self.schema.get_all_operations(generation_config=self.generation_config))
238
- generator_done = threading.Event()
239
- tasks_queue: Queue = Queue()
240
- # Add at least `workers_num` tasks first, so all workers are busy
241
- for _ in range(self.workers_num):
242
- try:
243
- # SAFETY: Workers didn't start yet, direct modification is OK
244
- tasks_queue.queue.append(next(tasks_generator))
245
- except StopIteration:
246
- generator_done.set()
247
- break
248
- # Events are pushed by workers via a separate queue
249
- events_queue: Queue = Queue()
250
- workers = self._init_workers(tasks_queue, events_queue, ctx, generator_done)
251
-
252
- def stop_workers() -> None:
253
- for worker in workers:
254
- # workers are initialized at this point and `worker.ident` is set with an integer value
255
- ident = cast(int, worker.ident)
256
- stop_worker(ident)
257
- worker.join()
258
-
259
- is_finished = False
260
- try:
261
- while not is_finished:
262
- # Sleep is needed for performance reasons
263
- # each call to `is_alive` of an alive worker waits for a lock
264
- # iterations without waiting are too frequent, and a lot of time will be spent on waiting for this locks
265
- time.sleep(0.001)
266
- is_finished = all(not worker.is_alive() for worker in workers)
267
- while not events_queue.empty():
268
- event = events_queue.get()
269
- if ctx.is_stopped or isinstance(event, events.Interrupted) or self._should_stop(event):
270
- # We could still have events in the queue, but ignore them to keep the logic simple
271
- # for now, could be improved in the future to show more info in such corner cases
272
- stop_workers()
273
- is_finished = True
274
- if ctx.is_stopped:
275
- # Discard the event. The invariant is: the next event after `stream.stop()` is `Finished`
276
- break
277
- yield event
278
- # When we know that there are more tasks, put another task to the queue.
279
- # The worker might not actually finish the current one yet, but we put the new one now, so
280
- # the worker can immediately pick it up when the current one is done
281
- if isinstance(event, events.BeforeExecution) and not generator_done.is_set():
282
- try:
283
- tasks_queue.put(next(tasks_generator))
284
- except StopIteration:
285
- generator_done.set()
286
- except KeyboardInterrupt:
287
- stop_workers()
288
- yield events.Interrupted()
289
-
290
- def _init_workers(
291
- self, tasks_queue: Queue, events_queue: Queue, ctx: RunnerContext, generator_done: threading.Event
292
- ) -> list[threading.Thread]:
293
- """Initialize & start workers that will execute tests."""
294
- workers = [
295
- threading.Thread(
296
- target=self._get_task(),
297
- kwargs=self._get_worker_kwargs(tasks_queue, events_queue, ctx, generator_done),
298
- name=f"schemathesis_{num}",
299
- )
300
- for num in range(self.workers_num)
301
- ]
302
- for worker in workers:
303
- worker.start()
304
- return workers
305
-
306
- def _get_task(self) -> Callable:
307
- return thread_task
308
-
309
- def _get_worker_kwargs(
310
- self, tasks_queue: Queue, events_queue: Queue, ctx: RunnerContext, generator_done: threading.Event
311
- ) -> dict[str, Any]:
312
- return {
313
- "tasks_queue": tasks_queue,
314
- "events_queue": events_queue,
315
- "generator_done": generator_done,
316
- "checks": self.checks,
317
- "targets": self.targets,
318
- "settings": self.hypothesis_settings,
319
- "generation_config": self.generation_config,
320
- "auth": self.auth,
321
- "auth_type": self.auth_type,
322
- "headers": self.headers,
323
- "ctx": ctx,
324
- "stateful": self.stateful,
325
- "stateful_recursion_limit": self.stateful_recursion_limit,
326
- "data_generation_methods": self.schema.data_generation_methods,
327
- "kwargs": {
328
- "request_config": self.request_config,
329
- "store_interactions": self.store_interactions,
330
- "max_response_time": self.max_response_time,
331
- "dry_run": self.dry_run,
332
- },
333
- }
334
-
335
-
336
- class ThreadPoolWSGIRunner(ThreadPoolRunner):
337
- def _get_task(self) -> Callable:
338
- return wsgi_thread_task
339
-
340
- def _get_worker_kwargs(
341
- self, tasks_queue: Queue, events_queue: Queue, ctx: RunnerContext, generator_done: threading.Event
342
- ) -> dict[str, Any]:
343
- return {
344
- "tasks_queue": tasks_queue,
345
- "events_queue": events_queue,
346
- "generator_done": generator_done,
347
- "checks": self.checks,
348
- "targets": self.targets,
349
- "settings": self.hypothesis_settings,
350
- "generation_config": self.generation_config,
351
- "ctx": ctx,
352
- "stateful": self.stateful,
353
- "stateful_recursion_limit": self.stateful_recursion_limit,
354
- "data_generation_methods": self.schema.data_generation_methods,
355
- "kwargs": {
356
- "auth": self.auth,
357
- "auth_type": self.auth_type,
358
- "headers": self.headers,
359
- "store_interactions": self.store_interactions,
360
- "max_response_time": self.max_response_time,
361
- "dry_run": self.dry_run,
362
- },
363
- }
364
-
365
-
366
- class ThreadPoolASGIRunner(ThreadPoolRunner):
367
- def _get_task(self) -> Callable:
368
- return asgi_thread_task
369
-
370
- def _get_worker_kwargs(
371
- self, tasks_queue: Queue, events_queue: Queue, ctx: RunnerContext, generator_done: threading.Event
372
- ) -> dict[str, Any]:
373
- return {
374
- "tasks_queue": tasks_queue,
375
- "events_queue": events_queue,
376
- "generator_done": generator_done,
377
- "checks": self.checks,
378
- "targets": self.targets,
379
- "settings": self.hypothesis_settings,
380
- "generation_config": self.generation_config,
381
- "headers": self.headers,
382
- "ctx": ctx,
383
- "stateful": self.stateful,
384
- "stateful_recursion_limit": self.stateful_recursion_limit,
385
- "data_generation_methods": self.schema.data_generation_methods,
386
- "kwargs": {
387
- "store_interactions": self.store_interactions,
388
- "max_response_time": self.max_response_time,
389
- "dry_run": self.dry_run,
390
- },
391
- }