schemathesis 3.13.0__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 (245) 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 -1016
  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 +683 -247
  125. schemathesis/specs/graphql/__init__.py +0 -1
  126. schemathesis/specs/graphql/nodes.py +27 -0
  127. schemathesis/specs/graphql/scalars.py +86 -0
  128. schemathesis/specs/graphql/schemas.py +395 -123
  129. schemathesis/specs/graphql/validation.py +33 -0
  130. schemathesis/specs/openapi/__init__.py +9 -1
  131. schemathesis/specs/openapi/_hypothesis.py +578 -317
  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 +753 -74
  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 +117 -68
  154. schemathesis/specs/openapi/negative/mutations.py +294 -104
  155. schemathesis/specs/openapi/negative/utils.py +3 -6
  156. schemathesis/specs/openapi/patterns.py +458 -0
  157. schemathesis/specs/openapi/references.py +60 -81
  158. schemathesis/specs/openapi/schemas.py +648 -650
  159. schemathesis/specs/openapi/serialization.py +53 -30
  160. schemathesis/specs/openapi/stateful/__init__.py +404 -69
  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.13.0.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.13.0.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
  188. schemathesis/_compat.py +0 -41
  189. schemathesis/_hypothesis.py +0 -115
  190. schemathesis/cli/callbacks.py +0 -188
  191. schemathesis/cli/cassettes.py +0 -253
  192. schemathesis/cli/context.py +0 -36
  193. schemathesis/cli/debug.py +0 -21
  194. schemathesis/cli/handlers.py +0 -11
  195. schemathesis/cli/junitxml.py +0 -41
  196. schemathesis/cli/options.py +0 -51
  197. schemathesis/cli/output/__init__.py +0 -1
  198. schemathesis/cli/output/default.py +0 -508
  199. schemathesis/cli/output/short.py +0 -40
  200. schemathesis/constants.py +0 -79
  201. schemathesis/exceptions.py +0 -207
  202. schemathesis/extra/_aiohttp.py +0 -27
  203. schemathesis/extra/_flask.py +0 -10
  204. schemathesis/extra/_server.py +0 -16
  205. schemathesis/extra/pytest_plugin.py +0 -216
  206. schemathesis/failures.py +0 -131
  207. schemathesis/fixups/__init__.py +0 -29
  208. schemathesis/fixups/fast_api.py +0 -30
  209. schemathesis/lazy.py +0 -227
  210. schemathesis/models.py +0 -1041
  211. schemathesis/parameters.py +0 -88
  212. schemathesis/runner/__init__.py +0 -460
  213. schemathesis/runner/events.py +0 -240
  214. schemathesis/runner/impl/__init__.py +0 -3
  215. schemathesis/runner/impl/core.py +0 -755
  216. schemathesis/runner/impl/solo.py +0 -85
  217. schemathesis/runner/impl/threadpool.py +0 -367
  218. schemathesis/runner/serialization.py +0 -189
  219. schemathesis/serializers.py +0 -233
  220. schemathesis/service/__init__.py +0 -3
  221. schemathesis/service/client.py +0 -46
  222. schemathesis/service/constants.py +0 -12
  223. schemathesis/service/events.py +0 -39
  224. schemathesis/service/handler.py +0 -39
  225. schemathesis/service/models.py +0 -7
  226. schemathesis/service/serialization.py +0 -153
  227. schemathesis/service/worker.py +0 -40
  228. schemathesis/specs/graphql/loaders.py +0 -215
  229. schemathesis/specs/openapi/constants.py +0 -7
  230. schemathesis/specs/openapi/expressions/context.py +0 -12
  231. schemathesis/specs/openapi/expressions/pointers.py +0 -29
  232. schemathesis/specs/openapi/filters.py +0 -44
  233. schemathesis/specs/openapi/links.py +0 -302
  234. schemathesis/specs/openapi/loaders.py +0 -453
  235. schemathesis/specs/openapi/parameters.py +0 -413
  236. schemathesis/specs/openapi/security.py +0 -129
  237. schemathesis/specs/openapi/validation.py +0 -24
  238. schemathesis/stateful.py +0 -349
  239. schemathesis/targets.py +0 -32
  240. schemathesis/types.py +0 -38
  241. schemathesis/utils.py +0 -436
  242. schemathesis-3.13.0.dist-info/METADATA +0 -202
  243. schemathesis-3.13.0.dist-info/RECORD +0 -91
  244. schemathesis-3.13.0.dist-info/entry_points.txt +0 -6
  245. /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,189 +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
12
- from ..failures import ValidationErrorContext
13
- from ..models import Case, Check, Interaction, Request, Response, Status, TestResult
14
- from ..utils import WSGIResponse, format_exception
15
-
16
-
17
- @attr.s(slots=True) # pragma: no mutate
18
- class SerializedCase:
19
- text_lines: List[str] = attr.ib() # pragma: no mutate
20
- requests_code: str = attr.ib()
21
- curl_code: str = attr.ib()
22
- path_template: str = attr.ib()
23
- path_parameters: Optional[Dict[str, Any]] = attr.ib()
24
- query: Optional[Dict[str, Any]] = attr.ib()
25
- cookies: Optional[Dict[str, Any]] = attr.ib()
26
- verbose_name: str = attr.ib()
27
- media_type: Optional[str] = attr.ib()
28
-
29
- @classmethod
30
- def from_case(cls, case: Case, headers: Optional[Dict[str, Any]]) -> "SerializedCase":
31
- return cls(
32
- text_lines=case.as_text_lines(headers),
33
- requests_code=case.get_code_to_reproduce(headers),
34
- curl_code=case.as_curl_command(headers),
35
- path_template=case.path,
36
- path_parameters=case.path_parameters,
37
- query=case.query,
38
- cookies=case.cookies,
39
- verbose_name=case.operation.verbose_name,
40
- media_type=case.media_type,
41
- )
42
-
43
-
44
- @attr.s(slots=True) # pragma: no mutate
45
- class SerializedCheck:
46
- # Check name
47
- name: str = attr.ib() # pragma: no mutate
48
- # Check result
49
- value: Status = attr.ib() # pragma: no mutate
50
- request: Request = attr.ib() # pragma: no mutate
51
- response: Optional[Response] = attr.ib() # pragma: no mutate
52
- # Generated example
53
- example: SerializedCase = attr.ib() # pragma: no mutate
54
- message: Optional[str] = attr.ib(default=None) # pragma: no mutate
55
- # Failure-specific context
56
- context: Optional[FailureContext] = attr.ib(default=None) # pragma: no mutate
57
-
58
- @classmethod
59
- def from_check(cls, check: Check) -> "SerializedCheck":
60
- if check.response is not None:
61
- request = Request.from_prepared_request(check.response.request)
62
- elif check.request is not None:
63
- # Response is not available, but it is not an error (only time-out behaves this way at the moment)
64
- request = Request.from_prepared_request(check.request)
65
- else:
66
- raise InternalError("Can not find request data")
67
-
68
- response: Optional[Response]
69
- if isinstance(check.response, requests.Response):
70
- response = Response.from_requests(check.response)
71
- elif isinstance(check.response, WSGIResponse):
72
- response = Response.from_wsgi(check.response, check.elapsed)
73
- else:
74
- response = None
75
- headers = {key: value[0] for key, value in request.headers.items()}
76
- return cls(
77
- name=check.name,
78
- value=check.value,
79
- example=SerializedCase.from_case(check.example, headers),
80
- message=check.message,
81
- request=request,
82
- response=response,
83
- context=check.context,
84
- )
85
-
86
-
87
- @attr.s(slots=True) # pragma: no mutate
88
- class SerializedError:
89
- exception: str = attr.ib() # pragma: no mutate
90
- exception_with_traceback: str = attr.ib() # pragma: no mutate
91
- example: Optional[SerializedCase] = attr.ib() # pragma: no mutate
92
- title: Optional[str] = attr.ib() # pragma: no mutate
93
-
94
- @classmethod
95
- def from_error(
96
- cls, exception: Exception, case: Optional[Case], headers: Optional[Dict[str, Any]], title: Optional[str] = None
97
- ) -> "SerializedError":
98
- return cls(
99
- exception=format_exception(exception),
100
- exception_with_traceback=format_exception(exception, True),
101
- example=SerializedCase.from_case(case, headers) if case else None,
102
- title=title,
103
- )
104
-
105
-
106
- @attr.s(slots=True) # pragma: no mutate
107
- class SerializedInteraction:
108
- request: Request = attr.ib() # pragma: no mutate
109
- response: Response = attr.ib() # pragma: no mutate
110
- checks: List[SerializedCheck] = attr.ib() # pragma: no mutate
111
- status: Status = attr.ib() # pragma: no mutate
112
- recorded_at: str = attr.ib() # pragma: no mutate
113
-
114
- @classmethod
115
- def from_interaction(cls, interaction: Interaction) -> "SerializedInteraction":
116
- return cls(
117
- request=interaction.request,
118
- response=interaction.response,
119
- checks=[SerializedCheck.from_check(check) for check in interaction.checks],
120
- status=interaction.status,
121
- recorded_at=interaction.recorded_at,
122
- )
123
-
124
-
125
- @attr.s(slots=True) # pragma: no mutate
126
- class SerializedTestResult:
127
- method: str = attr.ib() # pragma: no mutate
128
- path: str = attr.ib() # pragma: no mutate
129
- verbose_name: str = attr.ib() # pragma: no mutate
130
- has_failures: bool = attr.ib() # pragma: no mutate
131
- has_errors: bool = attr.ib() # pragma: no mutate
132
- has_logs: bool = attr.ib() # pragma: no mutate
133
- is_errored: bool = attr.ib() # pragma: no mutate
134
- seed: Optional[int] = attr.ib() # pragma: no mutate
135
- data_generation_method: str = attr.ib() # pragma: no mutate
136
- checks: List[SerializedCheck] = attr.ib() # pragma: no mutate
137
- logs: List[str] = attr.ib() # pragma: no mutate
138
- errors: List[SerializedError] = attr.ib() # pragma: no mutate
139
- interactions: List[SerializedInteraction] = attr.ib() # pragma: no mutate
140
-
141
- @classmethod
142
- def from_test_result(cls, result: TestResult) -> "SerializedTestResult":
143
- formatter = logging.Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s")
144
- return cls(
145
- method=result.method,
146
- path=result.path,
147
- verbose_name=result.verbose_name,
148
- has_failures=result.has_failures,
149
- has_errors=result.has_errors,
150
- has_logs=result.has_logs,
151
- is_errored=result.is_errored,
152
- seed=result.seed,
153
- data_generation_method=result.data_generation_method.as_short_name(),
154
- checks=[SerializedCheck.from_check(check) for check in result.checks],
155
- logs=[formatter.format(record) for record in result.logs],
156
- errors=[SerializedError.from_error(*error, headers=result.overridden_headers) for error in result.errors],
157
- interactions=[SerializedInteraction.from_interaction(interaction) for interaction in result.interactions],
158
- )
159
-
160
-
161
- def deduplicate_failures(checks: List[SerializedCheck]) -> List[SerializedCheck]:
162
- """Return only unique checks that should be displayed in the output."""
163
- seen: Set[Tuple[str, Optional[str]]] = set()
164
- unique_checks = []
165
- for check in reversed(checks):
166
- # There are also could be checks that didn't fail
167
- if check.value == Status.failure:
168
- key = get_check_key(check)
169
- if (check.name, key) not in seen:
170
- unique_checks.append(check)
171
- seen.add((check.name, key))
172
- return unique_checks
173
-
174
-
175
- def deduplicate_checks(checks: List[SerializedCheck]) -> Generator[SerializedCheck, None, None]:
176
- """Return only unique checks outcomes."""
177
- seen: Set[Tuple[str, Optional[str]]] = set()
178
- for check in reversed(checks):
179
- key = get_check_key(check)
180
- if (check.name, key) not in seen:
181
- yield check
182
- seen.add((check.name, key))
183
-
184
-
185
- def get_check_key(check: SerializedCheck) -> Optional[str]:
186
- if isinstance(check.context, ValidationErrorContext):
187
- # Deduplicate by JSON Schema path. All errors that happened on this sub-schema will be deduplicated
188
- return "/".join(map(str, check.context.schema_path))
189
- return check.message