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

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