schemathesis 3.15.4__py3-none-any.whl → 4.4.2__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 (251) hide show
  1. schemathesis/__init__.py +53 -25
  2. schemathesis/auths.py +507 -0
  3. schemathesis/checks.py +190 -25
  4. schemathesis/cli/__init__.py +27 -1219
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +133 -0
  7. schemathesis/cli/commands/data.py +10 -0
  8. schemathesis/cli/commands/run/__init__.py +602 -0
  9. schemathesis/cli/commands/run/context.py +228 -0
  10. schemathesis/cli/commands/run/events.py +60 -0
  11. schemathesis/cli/commands/run/executor.py +157 -0
  12. schemathesis/cli/commands/run/filters.py +53 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +45 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
  17. schemathesis/cli/commands/run/handlers/output.py +1750 -0
  18. schemathesis/cli/commands/run/loaders.py +118 -0
  19. schemathesis/cli/commands/run/validation.py +256 -0
  20. schemathesis/cli/constants.py +5 -0
  21. schemathesis/cli/core.py +19 -0
  22. schemathesis/cli/ext/fs.py +16 -0
  23. schemathesis/cli/ext/groups.py +203 -0
  24. schemathesis/cli/ext/options.py +81 -0
  25. schemathesis/config/__init__.py +202 -0
  26. schemathesis/config/_auth.py +51 -0
  27. schemathesis/config/_checks.py +268 -0
  28. schemathesis/config/_diff_base.py +101 -0
  29. schemathesis/config/_env.py +21 -0
  30. schemathesis/config/_error.py +163 -0
  31. schemathesis/config/_generation.py +157 -0
  32. schemathesis/config/_health_check.py +24 -0
  33. schemathesis/config/_operations.py +335 -0
  34. schemathesis/config/_output.py +171 -0
  35. schemathesis/config/_parameters.py +19 -0
  36. schemathesis/config/_phases.py +253 -0
  37. schemathesis/config/_projects.py +543 -0
  38. schemathesis/config/_rate_limit.py +17 -0
  39. schemathesis/config/_report.py +120 -0
  40. schemathesis/config/_validator.py +9 -0
  41. schemathesis/config/_warnings.py +89 -0
  42. schemathesis/config/schema.json +975 -0
  43. schemathesis/core/__init__.py +72 -0
  44. schemathesis/core/adapter.py +34 -0
  45. schemathesis/core/compat.py +32 -0
  46. schemathesis/core/control.py +2 -0
  47. schemathesis/core/curl.py +100 -0
  48. schemathesis/core/deserialization.py +210 -0
  49. schemathesis/core/errors.py +588 -0
  50. schemathesis/core/failures.py +316 -0
  51. schemathesis/core/fs.py +19 -0
  52. schemathesis/core/hooks.py +20 -0
  53. schemathesis/core/jsonschema/__init__.py +13 -0
  54. schemathesis/core/jsonschema/bundler.py +183 -0
  55. schemathesis/core/jsonschema/keywords.py +40 -0
  56. schemathesis/core/jsonschema/references.py +222 -0
  57. schemathesis/core/jsonschema/types.py +41 -0
  58. schemathesis/core/lazy_import.py +15 -0
  59. schemathesis/core/loaders.py +107 -0
  60. schemathesis/core/marks.py +66 -0
  61. schemathesis/core/media_types.py +79 -0
  62. schemathesis/core/output/__init__.py +46 -0
  63. schemathesis/core/output/sanitization.py +54 -0
  64. schemathesis/core/parameters.py +45 -0
  65. schemathesis/core/rate_limit.py +60 -0
  66. schemathesis/core/registries.py +34 -0
  67. schemathesis/core/result.py +27 -0
  68. schemathesis/core/schema_analysis.py +17 -0
  69. schemathesis/core/shell.py +203 -0
  70. schemathesis/core/transforms.py +144 -0
  71. schemathesis/core/transport.py +223 -0
  72. schemathesis/core/validation.py +73 -0
  73. schemathesis/core/version.py +7 -0
  74. schemathesis/engine/__init__.py +28 -0
  75. schemathesis/engine/context.py +152 -0
  76. schemathesis/engine/control.py +44 -0
  77. schemathesis/engine/core.py +201 -0
  78. schemathesis/engine/errors.py +446 -0
  79. schemathesis/engine/events.py +284 -0
  80. schemathesis/engine/observations.py +42 -0
  81. schemathesis/engine/phases/__init__.py +108 -0
  82. schemathesis/engine/phases/analysis.py +28 -0
  83. schemathesis/engine/phases/probes.py +172 -0
  84. schemathesis/engine/phases/stateful/__init__.py +68 -0
  85. schemathesis/engine/phases/stateful/_executor.py +364 -0
  86. schemathesis/engine/phases/stateful/context.py +85 -0
  87. schemathesis/engine/phases/unit/__init__.py +220 -0
  88. schemathesis/engine/phases/unit/_executor.py +459 -0
  89. schemathesis/engine/phases/unit/_pool.py +82 -0
  90. schemathesis/engine/recorder.py +254 -0
  91. schemathesis/errors.py +47 -0
  92. schemathesis/filters.py +395 -0
  93. schemathesis/generation/__init__.py +25 -0
  94. schemathesis/generation/case.py +478 -0
  95. schemathesis/generation/coverage.py +1528 -0
  96. schemathesis/generation/hypothesis/__init__.py +121 -0
  97. schemathesis/generation/hypothesis/builder.py +992 -0
  98. schemathesis/generation/hypothesis/examples.py +56 -0
  99. schemathesis/generation/hypothesis/given.py +66 -0
  100. schemathesis/generation/hypothesis/reporting.py +285 -0
  101. schemathesis/generation/meta.py +227 -0
  102. schemathesis/generation/metrics.py +93 -0
  103. schemathesis/generation/modes.py +20 -0
  104. schemathesis/generation/overrides.py +127 -0
  105. schemathesis/generation/stateful/__init__.py +37 -0
  106. schemathesis/generation/stateful/state_machine.py +294 -0
  107. schemathesis/graphql/__init__.py +15 -0
  108. schemathesis/graphql/checks.py +109 -0
  109. schemathesis/graphql/loaders.py +285 -0
  110. schemathesis/hooks.py +270 -91
  111. schemathesis/openapi/__init__.py +13 -0
  112. schemathesis/openapi/checks.py +467 -0
  113. schemathesis/openapi/generation/__init__.py +0 -0
  114. schemathesis/openapi/generation/filters.py +72 -0
  115. schemathesis/openapi/loaders.py +315 -0
  116. schemathesis/pytest/__init__.py +5 -0
  117. schemathesis/pytest/control_flow.py +7 -0
  118. schemathesis/pytest/lazy.py +341 -0
  119. schemathesis/pytest/loaders.py +36 -0
  120. schemathesis/pytest/plugin.py +357 -0
  121. schemathesis/python/__init__.py +0 -0
  122. schemathesis/python/asgi.py +12 -0
  123. schemathesis/python/wsgi.py +12 -0
  124. schemathesis/schemas.py +682 -257
  125. schemathesis/specs/graphql/__init__.py +0 -1
  126. schemathesis/specs/graphql/nodes.py +26 -2
  127. schemathesis/specs/graphql/scalars.py +77 -12
  128. schemathesis/specs/graphql/schemas.py +367 -148
  129. schemathesis/specs/graphql/validation.py +33 -0
  130. schemathesis/specs/openapi/__init__.py +9 -1
  131. schemathesis/specs/openapi/_hypothesis.py +555 -318
  132. schemathesis/specs/openapi/adapter/__init__.py +10 -0
  133. schemathesis/specs/openapi/adapter/parameters.py +729 -0
  134. schemathesis/specs/openapi/adapter/protocol.py +59 -0
  135. schemathesis/specs/openapi/adapter/references.py +19 -0
  136. schemathesis/specs/openapi/adapter/responses.py +368 -0
  137. schemathesis/specs/openapi/adapter/security.py +144 -0
  138. schemathesis/specs/openapi/adapter/v2.py +30 -0
  139. schemathesis/specs/openapi/adapter/v3_0.py +30 -0
  140. schemathesis/specs/openapi/adapter/v3_1.py +30 -0
  141. schemathesis/specs/openapi/analysis.py +96 -0
  142. schemathesis/specs/openapi/checks.py +748 -82
  143. schemathesis/specs/openapi/converter.py +176 -37
  144. schemathesis/specs/openapi/definitions.py +599 -4
  145. schemathesis/specs/openapi/examples.py +581 -165
  146. schemathesis/specs/openapi/expressions/__init__.py +52 -5
  147. schemathesis/specs/openapi/expressions/extractors.py +25 -0
  148. schemathesis/specs/openapi/expressions/lexer.py +34 -31
  149. schemathesis/specs/openapi/expressions/nodes.py +97 -46
  150. schemathesis/specs/openapi/expressions/parser.py +35 -13
  151. schemathesis/specs/openapi/formats.py +122 -0
  152. schemathesis/specs/openapi/media_types.py +75 -0
  153. schemathesis/specs/openapi/negative/__init__.py +93 -73
  154. schemathesis/specs/openapi/negative/mutations.py +294 -103
  155. schemathesis/specs/openapi/negative/utils.py +0 -9
  156. schemathesis/specs/openapi/patterns.py +458 -0
  157. schemathesis/specs/openapi/references.py +60 -81
  158. schemathesis/specs/openapi/schemas.py +647 -666
  159. schemathesis/specs/openapi/serialization.py +53 -30
  160. schemathesis/specs/openapi/stateful/__init__.py +403 -68
  161. schemathesis/specs/openapi/stateful/control.py +87 -0
  162. schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
  163. schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
  164. schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
  165. schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
  166. schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
  167. schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
  168. schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
  169. schemathesis/specs/openapi/stateful/inference.py +254 -0
  170. schemathesis/specs/openapi/stateful/links.py +219 -78
  171. schemathesis/specs/openapi/types/__init__.py +3 -0
  172. schemathesis/specs/openapi/types/common.py +23 -0
  173. schemathesis/specs/openapi/types/v2.py +129 -0
  174. schemathesis/specs/openapi/types/v3.py +134 -0
  175. schemathesis/specs/openapi/utils.py +7 -6
  176. schemathesis/specs/openapi/warnings.py +75 -0
  177. schemathesis/transport/__init__.py +224 -0
  178. schemathesis/transport/asgi.py +26 -0
  179. schemathesis/transport/prepare.py +126 -0
  180. schemathesis/transport/requests.py +278 -0
  181. schemathesis/transport/serialization.py +329 -0
  182. schemathesis/transport/wsgi.py +175 -0
  183. schemathesis-4.4.2.dist-info/METADATA +213 -0
  184. schemathesis-4.4.2.dist-info/RECORD +192 -0
  185. {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
  186. schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
  187. {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
  188. schemathesis/_compat.py +0 -57
  189. schemathesis/_hypothesis.py +0 -123
  190. schemathesis/auth.py +0 -214
  191. schemathesis/cli/callbacks.py +0 -240
  192. schemathesis/cli/cassettes.py +0 -351
  193. schemathesis/cli/context.py +0 -38
  194. schemathesis/cli/debug.py +0 -21
  195. schemathesis/cli/handlers.py +0 -11
  196. schemathesis/cli/junitxml.py +0 -41
  197. schemathesis/cli/options.py +0 -70
  198. schemathesis/cli/output/__init__.py +0 -1
  199. schemathesis/cli/output/default.py +0 -521
  200. schemathesis/cli/output/short.py +0 -40
  201. schemathesis/constants.py +0 -88
  202. schemathesis/exceptions.py +0 -257
  203. schemathesis/extra/_aiohttp.py +0 -27
  204. schemathesis/extra/_flask.py +0 -10
  205. schemathesis/extra/_server.py +0 -16
  206. schemathesis/extra/pytest_plugin.py +0 -251
  207. schemathesis/failures.py +0 -145
  208. schemathesis/fixups/__init__.py +0 -29
  209. schemathesis/fixups/fast_api.py +0 -30
  210. schemathesis/graphql.py +0 -5
  211. schemathesis/internal.py +0 -6
  212. schemathesis/lazy.py +0 -301
  213. schemathesis/models.py +0 -1113
  214. schemathesis/parameters.py +0 -91
  215. schemathesis/runner/__init__.py +0 -470
  216. schemathesis/runner/events.py +0 -242
  217. schemathesis/runner/impl/__init__.py +0 -3
  218. schemathesis/runner/impl/core.py +0 -791
  219. schemathesis/runner/impl/solo.py +0 -85
  220. schemathesis/runner/impl/threadpool.py +0 -367
  221. schemathesis/runner/serialization.py +0 -206
  222. schemathesis/serializers.py +0 -253
  223. schemathesis/service/__init__.py +0 -18
  224. schemathesis/service/auth.py +0 -10
  225. schemathesis/service/client.py +0 -62
  226. schemathesis/service/constants.py +0 -25
  227. schemathesis/service/events.py +0 -39
  228. schemathesis/service/handler.py +0 -46
  229. schemathesis/service/hosts.py +0 -74
  230. schemathesis/service/metadata.py +0 -42
  231. schemathesis/service/models.py +0 -21
  232. schemathesis/service/serialization.py +0 -184
  233. schemathesis/service/worker.py +0 -39
  234. schemathesis/specs/graphql/loaders.py +0 -215
  235. schemathesis/specs/openapi/constants.py +0 -7
  236. schemathesis/specs/openapi/expressions/context.py +0 -12
  237. schemathesis/specs/openapi/expressions/pointers.py +0 -29
  238. schemathesis/specs/openapi/filters.py +0 -44
  239. schemathesis/specs/openapi/links.py +0 -303
  240. schemathesis/specs/openapi/loaders.py +0 -453
  241. schemathesis/specs/openapi/parameters.py +0 -430
  242. schemathesis/specs/openapi/security.py +0 -129
  243. schemathesis/specs/openapi/validation.py +0 -24
  244. schemathesis/stateful.py +0 -358
  245. schemathesis/targets.py +0 -32
  246. schemathesis/types.py +0 -38
  247. schemathesis/utils.py +0 -475
  248. schemathesis-3.15.4.dist-info/METADATA +0 -202
  249. schemathesis-3.15.4.dist-info/RECORD +0 -99
  250. schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
  251. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
@@ -1,85 +0,0 @@
1
- import threading
2
- from typing import Generator, Optional, Union
3
-
4
- import attr
5
-
6
- from ...models import TestResultSet
7
- from ...types import RequestCert
8
- from ...utils import get_requests_auth
9
- from .. import events
10
- from .core import BaseRunner, asgi_test, get_session, network_test, wsgi_test
11
-
12
-
13
- @attr.s(slots=True) # pragma: no mutate
14
- class SingleThreadRunner(BaseRunner):
15
- """Fast runner that runs tests sequentially in the main thread."""
16
-
17
- request_tls_verify: Union[bool, str] = attr.ib(default=True) # pragma: no mutate
18
- request_cert: Optional[RequestCert] = attr.ib(default=None) # pragma: no mutate
19
-
20
- def _execute(
21
- self, results: TestResultSet, stop_event: threading.Event
22
- ) -> Generator[events.ExecutionEvent, None, None]:
23
- for event in self._execute_impl(results):
24
- yield event
25
- if stop_event.is_set() or self._should_stop(event):
26
- break
27
-
28
- def _execute_impl(self, results: TestResultSet) -> Generator[events.ExecutionEvent, None, None]:
29
- auth = get_requests_auth(self.auth, self.auth_type)
30
- with get_session(auth) as session:
31
- yield from self._run_tests(
32
- self.schema.get_all_tests,
33
- network_test,
34
- self.hypothesis_settings,
35
- self.seed,
36
- checks=self.checks,
37
- max_response_time=self.max_response_time,
38
- targets=self.targets,
39
- results=results,
40
- session=session,
41
- headers=self.headers,
42
- request_timeout=self.request_timeout,
43
- request_tls_verify=self.request_tls_verify,
44
- request_cert=self.request_cert,
45
- store_interactions=self.store_interactions,
46
- dry_run=self.dry_run,
47
- )
48
-
49
-
50
- @attr.s(slots=True) # pragma: no mutate
51
- class SingleThreadWSGIRunner(SingleThreadRunner):
52
- def _execute_impl(self, results: TestResultSet) -> Generator[events.ExecutionEvent, None, None]:
53
- yield from self._run_tests(
54
- self.schema.get_all_tests,
55
- wsgi_test,
56
- self.hypothesis_settings,
57
- self.seed,
58
- checks=self.checks,
59
- max_response_time=self.max_response_time,
60
- targets=self.targets,
61
- results=results,
62
- auth=self.auth,
63
- auth_type=self.auth_type,
64
- headers=self.headers,
65
- store_interactions=self.store_interactions,
66
- dry_run=self.dry_run,
67
- )
68
-
69
-
70
- @attr.s(slots=True) # pragma: no mutate
71
- class SingleThreadASGIRunner(SingleThreadRunner):
72
- def _execute_impl(self, results: TestResultSet) -> Generator[events.ExecutionEvent, None, None]:
73
- yield from self._run_tests(
74
- self.schema.get_all_tests,
75
- asgi_test,
76
- self.hypothesis_settings,
77
- self.seed,
78
- checks=self.checks,
79
- max_response_time=self.max_response_time,
80
- targets=self.targets,
81
- results=results,
82
- headers=self.headers,
83
- store_interactions=self.store_interactions,
84
- dry_run=self.dry_run,
85
- )
@@ -1,367 +0,0 @@
1
- import ctypes
2
- import queue
3
- import threading
4
- import time
5
- from queue import Queue
6
- from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Union, cast
7
-
8
- import attr
9
- import hypothesis
10
-
11
- from ..._hypothesis import create_test
12
- from ...models import CheckFunction, TestResultSet
13
- from ...stateful import Feedback, Stateful
14
- from ...targets import Target
15
- from ...types import RawAuth, RequestCert
16
- from ...utils import Ok, capture_hypothesis_output, get_requests_auth
17
- from .. import events
18
- from .core import BaseRunner, asgi_test, get_session, handle_schema_error, network_test, run_test, wsgi_test
19
-
20
-
21
- def _run_task(
22
- test_template: Callable,
23
- tasks_queue: Queue,
24
- events_queue: Queue,
25
- generator_done: threading.Event,
26
- checks: Iterable[CheckFunction],
27
- targets: Iterable[Target],
28
- settings: hypothesis.settings,
29
- seed: Optional[int],
30
- results: TestResultSet,
31
- stateful: Optional[Stateful],
32
- stateful_recursion_limit: int,
33
- **kwargs: Any,
34
- ) -> None:
35
- def _run_tests(maker: Callable, recursion_level: int = 0) -> None:
36
- if recursion_level > stateful_recursion_limit:
37
- return
38
- for _result, _data_generation_method in maker(test_template, settings, seed):
39
- # `result` is always `Ok` here
40
- _operation, test = _result.ok()
41
- feedback = Feedback(stateful, _operation)
42
- for _event in run_test(
43
- _operation,
44
- test,
45
- checks,
46
- data_generation_method,
47
- targets,
48
- results,
49
- recursion_level=recursion_level,
50
- feedback=feedback,
51
- **kwargs,
52
- ):
53
- events_queue.put(_event)
54
- _run_tests(feedback.get_stateful_tests, recursion_level + 1)
55
-
56
- with capture_hypothesis_output():
57
- while True:
58
- try:
59
- result, data_generation_method = tasks_queue.get(timeout=0.001)
60
- except queue.Empty:
61
- # The queue is empty & there will be no more tasks
62
- if generator_done.is_set():
63
- break
64
- # If there is a possibility for new tasks - try again
65
- continue
66
- if isinstance(result, Ok):
67
- operation = result.ok()
68
- test_function = create_test(
69
- operation=operation,
70
- test=test_template,
71
- settings=settings,
72
- seed=seed,
73
- data_generation_method=data_generation_method,
74
- )
75
- items = (
76
- Ok((operation, test_function)),
77
- data_generation_method,
78
- )
79
- # This lambda ignores the input arguments to support the same interface for
80
- # `feedback.get_stateful_tests`
81
- _run_tests(lambda *_: (items,))
82
- else:
83
- for event in handle_schema_error(result.err(), results, data_generation_method, 0):
84
- events_queue.put(event)
85
-
86
-
87
- def thread_task(
88
- tasks_queue: Queue,
89
- events_queue: Queue,
90
- generator_done: threading.Event,
91
- checks: Iterable[CheckFunction],
92
- targets: Iterable[Target],
93
- settings: hypothesis.settings,
94
- auth: Optional[RawAuth],
95
- auth_type: Optional[str],
96
- headers: Optional[Dict[str, Any]],
97
- seed: Optional[int],
98
- results: TestResultSet,
99
- stateful: Optional[Stateful],
100
- stateful_recursion_limit: int,
101
- kwargs: Any,
102
- ) -> None:
103
- """A single task, that threads do.
104
-
105
- Pretty similar to the default one-thread flow, but includes communication with the main thread via the events queue.
106
- """
107
- prepared_auth = get_requests_auth(auth, auth_type)
108
- with get_session(prepared_auth) as session:
109
- _run_task(
110
- network_test,
111
- tasks_queue,
112
- events_queue,
113
- generator_done,
114
- checks,
115
- targets,
116
- settings,
117
- seed,
118
- results,
119
- stateful=stateful,
120
- stateful_recursion_limit=stateful_recursion_limit,
121
- session=session,
122
- headers=headers,
123
- **kwargs,
124
- )
125
-
126
-
127
- def wsgi_thread_task(
128
- tasks_queue: Queue,
129
- events_queue: Queue,
130
- generator_done: threading.Event,
131
- checks: Iterable[CheckFunction],
132
- targets: Iterable[Target],
133
- settings: hypothesis.settings,
134
- seed: Optional[int],
135
- results: TestResultSet,
136
- stateful: Optional[Stateful],
137
- stateful_recursion_limit: int,
138
- kwargs: Any,
139
- ) -> None:
140
- _run_task(
141
- wsgi_test,
142
- tasks_queue,
143
- events_queue,
144
- generator_done,
145
- checks,
146
- targets,
147
- settings,
148
- seed,
149
- results,
150
- stateful=stateful,
151
- stateful_recursion_limit=stateful_recursion_limit,
152
- **kwargs,
153
- )
154
-
155
-
156
- def asgi_thread_task(
157
- tasks_queue: Queue,
158
- events_queue: Queue,
159
- generator_done: threading.Event,
160
- checks: Iterable[CheckFunction],
161
- targets: Iterable[Target],
162
- settings: hypothesis.settings,
163
- headers: Optional[Dict[str, Any]],
164
- seed: Optional[int],
165
- results: TestResultSet,
166
- stateful: Optional[Stateful],
167
- stateful_recursion_limit: int,
168
- kwargs: Any,
169
- ) -> None:
170
- _run_task(
171
- asgi_test,
172
- tasks_queue,
173
- events_queue,
174
- generator_done,
175
- checks,
176
- targets,
177
- settings,
178
- seed,
179
- results,
180
- stateful=stateful,
181
- stateful_recursion_limit=stateful_recursion_limit,
182
- headers=headers,
183
- **kwargs,
184
- )
185
-
186
-
187
- def stop_worker(thread_id: int) -> None:
188
- """Raise an error in a thread, so it is possible to asynchronously stop thread execution."""
189
- ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(thread_id), ctypes.py_object(SystemExit))
190
-
191
-
192
- @attr.s(slots=True) # pragma: no mutate
193
- class ThreadPoolRunner(BaseRunner):
194
- """Spread different tests among multiple worker threads."""
195
-
196
- workers_num: int = attr.ib(default=2) # pragma: no mutate
197
- request_tls_verify: Union[bool, str] = attr.ib(default=True) # pragma: no mutate
198
- request_cert: Optional[RequestCert] = attr.ib(default=None) # pragma: no mutate
199
-
200
- def _execute(
201
- self, results: TestResultSet, stop_event: threading.Event
202
- ) -> Generator[events.ExecutionEvent, None, None]:
203
- """All events come from a queue where different workers push their events."""
204
- # Instead of generating all tests at once, we do it when there is a free worker to pick it up
205
- # This is extremely important for memory consumption when testing large schemas
206
- # IMPLEMENTATION NOTE:
207
- # It would be better to have a separate producer thread and communicate via threading events.
208
- # Though it is a bit more complex, so the current solution is suboptimal in terms of resources utilization,
209
- # but good enough and easy enough to implement.
210
- tasks_generator = (
211
- (operation, data_generation_method)
212
- for operation in self.schema.get_all_operations()
213
- for data_generation_method in self.schema.data_generation_methods
214
- )
215
- generator_done = threading.Event()
216
- tasks_queue: Queue = Queue()
217
- # Add at least `workers_num` tasks first, so all workers are busy
218
- for _ in range(self.workers_num):
219
- try:
220
- # SAFETY: Workers didn't start yet, direct modification is OK
221
- tasks_queue.queue.append(next(tasks_generator))
222
- except StopIteration:
223
- generator_done.set()
224
- break
225
- # Events are pushed by workers via a separate queue
226
- events_queue: Queue = Queue()
227
- workers = self._init_workers(tasks_queue, events_queue, results, generator_done)
228
-
229
- def stop_workers() -> None:
230
- for worker in workers:
231
- # workers are initialized at this point and `worker.ident` is set with an integer value
232
- ident = cast(int, worker.ident)
233
- stop_worker(ident)
234
- worker.join()
235
-
236
- is_finished = False
237
- try:
238
- while not is_finished:
239
- # Sleep is needed for performance reasons
240
- # each call to `is_alive` of an alive worker waits for a lock
241
- # iterations without waiting are too frequent, and a lot of time will be spent on waiting for this locks
242
- time.sleep(0.001)
243
- is_finished = all(not worker.is_alive() for worker in workers)
244
- while not events_queue.empty():
245
- event = events_queue.get()
246
- if stop_event.is_set() or isinstance(event, events.Interrupted) or self._should_stop(event):
247
- # We could still have events in the queue, but ignore them to keep the logic simple
248
- # for now, could be improved in the future to show more info in such corner cases
249
- stop_workers()
250
- is_finished = True
251
- if stop_event.is_set():
252
- # Discard the event. The invariant is: the next event after `stream.stop()` is `Finished`
253
- break
254
- yield event
255
- # When we know that there are more tasks, put another task to the queue.
256
- # The worker might not actually finish the current one yet, but we put the new one now, so
257
- # the worker can immediately pick it up when the current one is done
258
- if isinstance(event, events.BeforeExecution) and not generator_done.is_set():
259
- try:
260
- tasks_queue.put(next(tasks_generator))
261
- except StopIteration:
262
- generator_done.set()
263
- except KeyboardInterrupt:
264
- stop_workers()
265
- yield events.Interrupted()
266
-
267
- def _init_workers(
268
- self, tasks_queue: Queue, events_queue: Queue, results: TestResultSet, generator_done: threading.Event
269
- ) -> List[threading.Thread]:
270
- """Initialize & start workers that will execute tests."""
271
- workers = [
272
- threading.Thread(
273
- target=self._get_task(),
274
- kwargs=self._get_worker_kwargs(tasks_queue, events_queue, results, generator_done),
275
- name=f"schemathesis_{num}",
276
- )
277
- for num in range(self.workers_num)
278
- ]
279
- for worker in workers:
280
- worker.start()
281
- return workers
282
-
283
- def _get_task(self) -> Callable:
284
- return thread_task
285
-
286
- def _get_worker_kwargs(
287
- self, tasks_queue: Queue, events_queue: Queue, results: TestResultSet, generator_done: threading.Event
288
- ) -> Dict[str, Any]:
289
- return {
290
- "tasks_queue": tasks_queue,
291
- "events_queue": events_queue,
292
- "generator_done": generator_done,
293
- "checks": self.checks,
294
- "targets": self.targets,
295
- "settings": self.hypothesis_settings,
296
- "auth": self.auth,
297
- "auth_type": self.auth_type,
298
- "headers": self.headers,
299
- "seed": self.seed,
300
- "results": results,
301
- "stateful": self.stateful,
302
- "stateful_recursion_limit": self.stateful_recursion_limit,
303
- "kwargs": {
304
- "request_timeout": self.request_timeout,
305
- "request_tls_verify": self.request_tls_verify,
306
- "request_cert": self.request_cert,
307
- "store_interactions": self.store_interactions,
308
- "max_response_time": self.max_response_time,
309
- "dry_run": self.dry_run,
310
- },
311
- }
312
-
313
-
314
- class ThreadPoolWSGIRunner(ThreadPoolRunner):
315
- def _get_task(self) -> Callable:
316
- return wsgi_thread_task
317
-
318
- def _get_worker_kwargs(
319
- self, tasks_queue: Queue, events_queue: Queue, results: TestResultSet, generator_done: threading.Event
320
- ) -> Dict[str, Any]:
321
- return {
322
- "tasks_queue": tasks_queue,
323
- "events_queue": events_queue,
324
- "generator_done": generator_done,
325
- "checks": self.checks,
326
- "targets": self.targets,
327
- "settings": self.hypothesis_settings,
328
- "seed": self.seed,
329
- "results": results,
330
- "stateful": self.stateful,
331
- "stateful_recursion_limit": self.stateful_recursion_limit,
332
- "kwargs": {
333
- "auth": self.auth,
334
- "auth_type": self.auth_type,
335
- "headers": self.headers,
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 ThreadPoolASGIRunner(ThreadPoolRunner):
344
- def _get_task(self) -> Callable:
345
- return asgi_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
- "headers": self.headers,
358
- "seed": self.seed,
359
- "results": results,
360
- "stateful": self.stateful,
361
- "stateful_recursion_limit": self.stateful_recursion_limit,
362
- "kwargs": {
363
- "store_interactions": self.store_interactions,
364
- "max_response_time": self.max_response_time,
365
- "dry_run": self.dry_run,
366
- },
367
- }
@@ -1,206 +0,0 @@
1
- """Transformation from Schemathesis-specific data structures to ones that can be serialized and sent over network.
2
-
3
- They all consist of primitive types and don't have references to schemas, app, etc.
4
- """
5
- import logging
6
- from typing import Any, Dict, Generator, List, Optional, Set, Tuple
7
-
8
- import attr
9
- import requests
10
-
11
- from ..exceptions import FailureContext, InternalError, make_unique_by_key
12
- from ..models import Case, Check, Interaction, Request, Response, Status, TestResult
13
- from ..utils import WSGIResponse, format_exception
14
-
15
-
16
- @attr.s(slots=True) # pragma: no mutate
17
- class SerializedCase:
18
- text_lines: List[str] = attr.ib() # pragma: no mutate
19
- requests_code: str = attr.ib()
20
- curl_code: str = attr.ib()
21
- path_template: str = attr.ib()
22
- path_parameters: Optional[Dict[str, Any]] = attr.ib()
23
- query: Optional[Dict[str, Any]] = attr.ib()
24
- cookies: Optional[Dict[str, Any]] = attr.ib()
25
- verbose_name: str = attr.ib()
26
- media_type: Optional[str] = attr.ib()
27
-
28
- @classmethod
29
- def from_case(cls, case: Case, headers: Optional[Dict[str, Any]]) -> "SerializedCase":
30
- return cls(
31
- text_lines=case.as_text_lines(headers),
32
- requests_code=case.get_code_to_reproduce(headers),
33
- curl_code=case.as_curl_command(headers),
34
- path_template=case.path,
35
- path_parameters=case.path_parameters,
36
- query=case.query,
37
- cookies=case.cookies,
38
- verbose_name=case.operation.verbose_name,
39
- media_type=case.media_type,
40
- )
41
-
42
-
43
- @attr.s(slots=True) # pragma: no mutate
44
- class SerializedCheck:
45
- # Check name
46
- name: str = attr.ib() # pragma: no mutate
47
- # Check result
48
- value: Status = attr.ib() # pragma: no mutate
49
- request: Request = attr.ib() # pragma: no mutate
50
- response: Optional[Response] = attr.ib() # pragma: no mutate
51
- # Generated example
52
- example: SerializedCase = attr.ib() # pragma: no mutate
53
- message: Optional[str] = attr.ib(default=None) # pragma: no mutate
54
- # Failure-specific context
55
- context: Optional[FailureContext] = attr.ib(default=None) # pragma: no mutate
56
- # Cases & responses that were made before this one
57
- history: List["SerializedHistoryEntry"] = attr.ib(factory=list) # pragma: no mutate
58
-
59
- @classmethod
60
- def from_check(cls, check: Check) -> "SerializedCheck":
61
- if check.response is not None:
62
- request = Request.from_prepared_request(check.response.request)
63
- elif check.request is not None:
64
- # Response is not available, but it is not an error (only time-out behaves this way at the moment)
65
- request = Request.from_prepared_request(check.request)
66
- else:
67
- raise InternalError("Can not find request data")
68
-
69
- response: Optional[Response]
70
- if isinstance(check.response, requests.Response):
71
- response = Response.from_requests(check.response)
72
- elif isinstance(check.response, WSGIResponse):
73
- response = Response.from_wsgi(check.response, check.elapsed)
74
- else:
75
- response = None
76
- headers = {key: value[0] for key, value in request.headers.items()}
77
- history = []
78
- case = check.example
79
- while case.source is not None:
80
- if isinstance(case.source.response, requests.Response):
81
- history_response = Response.from_requests(case.source.response)
82
- else:
83
- history_response = Response.from_wsgi(case.source.response, case.source.elapsed)
84
- entry = SerializedHistoryEntry(
85
- case=SerializedCase.from_case(case.source.case, headers), response=history_response
86
- )
87
- history.append(entry)
88
- case = case.source.case
89
- return cls(
90
- name=check.name,
91
- value=check.value,
92
- example=SerializedCase.from_case(check.example, headers),
93
- message=check.message,
94
- request=request,
95
- response=response,
96
- context=check.context,
97
- history=history,
98
- )
99
-
100
-
101
- @attr.s(slots=True) # pragma: no mutate
102
- class SerializedHistoryEntry:
103
- case: SerializedCase = attr.ib()
104
- response: Response = attr.ib()
105
-
106
-
107
- @attr.s(slots=True) # pragma: no mutate
108
- class SerializedError:
109
- exception: str = attr.ib() # pragma: no mutate
110
- exception_with_traceback: str = attr.ib() # pragma: no mutate
111
- example: Optional[SerializedCase] = attr.ib() # pragma: no mutate
112
- title: Optional[str] = attr.ib() # pragma: no mutate
113
-
114
- @classmethod
115
- def from_error(
116
- cls, exception: Exception, case: Optional[Case], headers: Optional[Dict[str, Any]], title: Optional[str] = None
117
- ) -> "SerializedError":
118
- return cls(
119
- exception=format_exception(exception),
120
- exception_with_traceback=format_exception(exception, True),
121
- example=SerializedCase.from_case(case, headers) if case else None,
122
- title=title,
123
- )
124
-
125
-
126
- @attr.s(slots=True) # pragma: no mutate
127
- class SerializedInteraction:
128
- request: Request = attr.ib() # pragma: no mutate
129
- response: Response = attr.ib() # pragma: no mutate
130
- checks: List[SerializedCheck] = attr.ib() # pragma: no mutate
131
- status: Status = attr.ib() # pragma: no mutate
132
- recorded_at: str = attr.ib() # pragma: no mutate
133
-
134
- @classmethod
135
- def from_interaction(cls, interaction: Interaction) -> "SerializedInteraction":
136
- return cls(
137
- request=interaction.request,
138
- response=interaction.response,
139
- checks=[SerializedCheck.from_check(check) for check in interaction.checks],
140
- status=interaction.status,
141
- recorded_at=interaction.recorded_at,
142
- )
143
-
144
-
145
- @attr.s(slots=True) # pragma: no mutate
146
- class SerializedTestResult:
147
- method: str = attr.ib() # pragma: no mutate
148
- path: str = attr.ib() # pragma: no mutate
149
- verbose_name: str = attr.ib() # pragma: no mutate
150
- has_failures: bool = attr.ib() # pragma: no mutate
151
- has_errors: bool = attr.ib() # pragma: no mutate
152
- has_logs: bool = attr.ib() # pragma: no mutate
153
- is_errored: bool = attr.ib() # pragma: no mutate
154
- is_flaky: bool = attr.ib() # pragma: no mutate
155
- is_skipped: bool = attr.ib() # pragma: no mutate
156
- seed: Optional[int] = attr.ib() # pragma: no mutate
157
- data_generation_method: str = attr.ib() # pragma: no mutate
158
- checks: List[SerializedCheck] = attr.ib() # pragma: no mutate
159
- logs: List[str] = attr.ib() # pragma: no mutate
160
- errors: List[SerializedError] = attr.ib() # pragma: no mutate
161
- interactions: List[SerializedInteraction] = attr.ib() # pragma: no mutate
162
-
163
- @classmethod
164
- def from_test_result(cls, result: TestResult) -> "SerializedTestResult":
165
- formatter = logging.Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s")
166
- return cls(
167
- method=result.method,
168
- path=result.path,
169
- verbose_name=result.verbose_name,
170
- has_failures=result.has_failures,
171
- has_errors=result.has_errors,
172
- has_logs=result.has_logs,
173
- is_errored=result.is_errored,
174
- is_flaky=result.is_flaky,
175
- is_skipped=result.is_skipped,
176
- seed=result.seed,
177
- data_generation_method=result.data_generation_method.as_short_name(),
178
- checks=[SerializedCheck.from_check(check) for check in result.checks],
179
- logs=[formatter.format(record) for record in result.logs],
180
- errors=[SerializedError.from_error(*error, headers=result.overridden_headers) for error in result.errors],
181
- interactions=[SerializedInteraction.from_interaction(interaction) for interaction in result.interactions],
182
- )
183
-
184
-
185
- def deduplicate_failures(checks: List[SerializedCheck]) -> List[SerializedCheck]:
186
- """Return only unique checks that should be displayed in the output."""
187
- seen: Set[Tuple[Optional[str], ...]] = set()
188
- unique_checks = []
189
- for check in reversed(checks):
190
- # There are also could be checks that didn't fail
191
- if check.value == Status.failure:
192
- key = make_unique_by_key(check.name, check.message, check.context)
193
- if key not in seen:
194
- unique_checks.append(check)
195
- seen.add(key)
196
- return unique_checks
197
-
198
-
199
- def deduplicate_checks(checks: List[SerializedCheck]) -> Generator[SerializedCheck, None, None]:
200
- """Return only unique checks outcomes."""
201
- seen: Set[Tuple[Optional[str], ...]] = set()
202
- for check in reversed(checks):
203
- key = make_unique_by_key(check.name, check.message, check.context)
204
- if key not in seen:
205
- yield check
206
- seen.add(key)