schemathesis 3.39.7__py3-none-any.whl → 4.0.0a2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +26 -68
  3. schemathesis/checks.py +130 -60
  4. schemathesis/cli/__init__.py +5 -2105
  5. schemathesis/cli/commands/__init__.py +37 -0
  6. schemathesis/cli/commands/run/__init__.py +662 -0
  7. schemathesis/cli/commands/run/checks.py +80 -0
  8. schemathesis/cli/commands/run/context.py +117 -0
  9. schemathesis/cli/commands/run/events.py +30 -0
  10. schemathesis/cli/commands/run/executor.py +141 -0
  11. schemathesis/cli/commands/run/filters.py +202 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/{cassettes.py → commands/run/handlers/cassettes.py} +178 -247
  15. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1368 -0
  17. schemathesis/cli/commands/run/hypothesis.py +105 -0
  18. schemathesis/cli/commands/run/loaders.py +129 -0
  19. schemathesis/cli/{callbacks.py → commands/run/validation.py} +59 -175
  20. schemathesis/cli/constants.py +5 -58
  21. schemathesis/cli/core.py +17 -0
  22. schemathesis/cli/ext/fs.py +14 -0
  23. schemathesis/cli/ext/groups.py +55 -0
  24. schemathesis/cli/{options.py → ext/options.py} +37 -16
  25. schemathesis/cli/hooks.py +36 -0
  26. schemathesis/contrib/__init__.py +1 -3
  27. schemathesis/contrib/openapi/__init__.py +1 -3
  28. schemathesis/contrib/openapi/fill_missing_examples.py +3 -7
  29. schemathesis/core/__init__.py +58 -0
  30. schemathesis/core/compat.py +25 -0
  31. schemathesis/core/control.py +2 -0
  32. schemathesis/core/curl.py +58 -0
  33. schemathesis/core/deserialization.py +65 -0
  34. schemathesis/core/errors.py +370 -0
  35. schemathesis/core/failures.py +315 -0
  36. schemathesis/core/fs.py +19 -0
  37. schemathesis/core/loaders.py +104 -0
  38. schemathesis/core/marks.py +66 -0
  39. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  40. schemathesis/{internal/output.py → core/output/__init__.py} +1 -0
  41. schemathesis/core/output/sanitization.py +197 -0
  42. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  43. schemathesis/core/registries.py +31 -0
  44. schemathesis/core/transforms.py +113 -0
  45. schemathesis/core/transport.py +108 -0
  46. schemathesis/core/validation.py +38 -0
  47. schemathesis/core/version.py +7 -0
  48. schemathesis/engine/__init__.py +30 -0
  49. schemathesis/engine/config.py +59 -0
  50. schemathesis/engine/context.py +119 -0
  51. schemathesis/engine/control.py +36 -0
  52. schemathesis/engine/core.py +157 -0
  53. schemathesis/engine/errors.py +394 -0
  54. schemathesis/engine/events.py +243 -0
  55. schemathesis/engine/phases/__init__.py +66 -0
  56. schemathesis/{runner → engine/phases}/probes.py +49 -68
  57. schemathesis/engine/phases/stateful/__init__.py +66 -0
  58. schemathesis/engine/phases/stateful/_executor.py +301 -0
  59. schemathesis/engine/phases/stateful/context.py +85 -0
  60. schemathesis/engine/phases/unit/__init__.py +175 -0
  61. schemathesis/engine/phases/unit/_executor.py +322 -0
  62. schemathesis/engine/phases/unit/_pool.py +74 -0
  63. schemathesis/engine/recorder.py +246 -0
  64. schemathesis/errors.py +31 -0
  65. schemathesis/experimental/__init__.py +9 -40
  66. schemathesis/filters.py +7 -95
  67. schemathesis/generation/__init__.py +3 -3
  68. schemathesis/generation/case.py +190 -0
  69. schemathesis/generation/coverage.py +22 -22
  70. schemathesis/{_patches.py → generation/hypothesis/__init__.py} +15 -6
  71. schemathesis/generation/hypothesis/builder.py +585 -0
  72. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  73. schemathesis/generation/hypothesis/given.py +66 -0
  74. schemathesis/generation/hypothesis/reporting.py +14 -0
  75. schemathesis/generation/hypothesis/strategies.py +16 -0
  76. schemathesis/generation/meta.py +115 -0
  77. schemathesis/generation/modes.py +28 -0
  78. schemathesis/generation/overrides.py +96 -0
  79. schemathesis/generation/stateful/__init__.py +20 -0
  80. schemathesis/{stateful → generation/stateful}/state_machine.py +84 -109
  81. schemathesis/generation/targets.py +69 -0
  82. schemathesis/graphql/__init__.py +15 -0
  83. schemathesis/graphql/checks.py +109 -0
  84. schemathesis/graphql/loaders.py +131 -0
  85. schemathesis/hooks.py +17 -62
  86. schemathesis/openapi/__init__.py +13 -0
  87. schemathesis/openapi/checks.py +387 -0
  88. schemathesis/openapi/generation/__init__.py +0 -0
  89. schemathesis/openapi/generation/filters.py +63 -0
  90. schemathesis/openapi/loaders.py +178 -0
  91. schemathesis/pytest/__init__.py +5 -0
  92. schemathesis/pytest/control_flow.py +7 -0
  93. schemathesis/pytest/lazy.py +273 -0
  94. schemathesis/pytest/loaders.py +12 -0
  95. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +94 -107
  96. schemathesis/python/__init__.py +0 -0
  97. schemathesis/python/asgi.py +12 -0
  98. schemathesis/python/wsgi.py +12 -0
  99. schemathesis/schemas.py +456 -228
  100. schemathesis/specs/graphql/__init__.py +0 -1
  101. schemathesis/specs/graphql/_cache.py +1 -2
  102. schemathesis/specs/graphql/scalars.py +5 -3
  103. schemathesis/specs/graphql/schemas.py +122 -123
  104. schemathesis/specs/graphql/validation.py +11 -17
  105. schemathesis/specs/openapi/__init__.py +6 -1
  106. schemathesis/specs/openapi/_cache.py +1 -2
  107. schemathesis/specs/openapi/_hypothesis.py +97 -134
  108. schemathesis/specs/openapi/checks.py +238 -219
  109. schemathesis/specs/openapi/converter.py +4 -4
  110. schemathesis/specs/openapi/definitions.py +1 -1
  111. schemathesis/specs/openapi/examples.py +22 -20
  112. schemathesis/specs/openapi/expressions/__init__.py +11 -15
  113. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  114. schemathesis/specs/openapi/expressions/nodes.py +33 -32
  115. schemathesis/specs/openapi/formats.py +3 -2
  116. schemathesis/specs/openapi/links.py +123 -299
  117. schemathesis/specs/openapi/media_types.py +10 -12
  118. schemathesis/specs/openapi/negative/__init__.py +2 -1
  119. schemathesis/specs/openapi/negative/mutations.py +3 -2
  120. schemathesis/specs/openapi/parameters.py +8 -6
  121. schemathesis/specs/openapi/patterns.py +1 -1
  122. schemathesis/specs/openapi/references.py +11 -51
  123. schemathesis/specs/openapi/schemas.py +177 -191
  124. schemathesis/specs/openapi/security.py +1 -1
  125. schemathesis/specs/openapi/serialization.py +10 -6
  126. schemathesis/specs/openapi/stateful/__init__.py +97 -91
  127. schemathesis/transport/__init__.py +104 -0
  128. schemathesis/transport/asgi.py +26 -0
  129. schemathesis/transport/prepare.py +99 -0
  130. schemathesis/transport/requests.py +221 -0
  131. schemathesis/{_xml.py → transport/serialization.py} +69 -7
  132. schemathesis/transport/wsgi.py +165 -0
  133. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +18 -14
  134. schemathesis-4.0.0a2.dist-info/RECORD +151 -0
  135. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +1 -1
  136. schemathesis/_compat.py +0 -74
  137. schemathesis/_dependency_versions.py +0 -19
  138. schemathesis/_hypothesis.py +0 -559
  139. schemathesis/_override.py +0 -50
  140. schemathesis/_rate_limiter.py +0 -7
  141. schemathesis/cli/context.py +0 -75
  142. schemathesis/cli/debug.py +0 -27
  143. schemathesis/cli/handlers.py +0 -19
  144. schemathesis/cli/junitxml.py +0 -124
  145. schemathesis/cli/output/__init__.py +0 -1
  146. schemathesis/cli/output/default.py +0 -936
  147. schemathesis/cli/output/short.py +0 -59
  148. schemathesis/cli/reporting.py +0 -79
  149. schemathesis/cli/sanitization.py +0 -26
  150. schemathesis/code_samples.py +0 -151
  151. schemathesis/constants.py +0 -56
  152. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  153. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  154. schemathesis/contrib/unique_data.py +0 -41
  155. schemathesis/exceptions.py +0 -571
  156. schemathesis/extra/_aiohttp.py +0 -28
  157. schemathesis/extra/_flask.py +0 -13
  158. schemathesis/extra/_server.py +0 -18
  159. schemathesis/failures.py +0 -277
  160. schemathesis/fixups/__init__.py +0 -37
  161. schemathesis/fixups/fast_api.py +0 -41
  162. schemathesis/fixups/utf8_bom.py +0 -28
  163. schemathesis/generation/_methods.py +0 -44
  164. schemathesis/graphql.py +0 -3
  165. schemathesis/internal/__init__.py +0 -7
  166. schemathesis/internal/checks.py +0 -84
  167. schemathesis/internal/copy.py +0 -32
  168. schemathesis/internal/datetime.py +0 -5
  169. schemathesis/internal/deprecation.py +0 -38
  170. schemathesis/internal/diff.py +0 -15
  171. schemathesis/internal/extensions.py +0 -27
  172. schemathesis/internal/jsonschema.py +0 -36
  173. schemathesis/internal/transformation.py +0 -26
  174. schemathesis/internal/validation.py +0 -34
  175. schemathesis/lazy.py +0 -474
  176. schemathesis/loaders.py +0 -122
  177. schemathesis/models.py +0 -1341
  178. schemathesis/parameters.py +0 -90
  179. schemathesis/runner/__init__.py +0 -605
  180. schemathesis/runner/events.py +0 -389
  181. schemathesis/runner/impl/__init__.py +0 -3
  182. schemathesis/runner/impl/context.py +0 -104
  183. schemathesis/runner/impl/core.py +0 -1246
  184. schemathesis/runner/impl/solo.py +0 -80
  185. schemathesis/runner/impl/threadpool.py +0 -391
  186. schemathesis/runner/serialization.py +0 -544
  187. schemathesis/sanitization.py +0 -252
  188. schemathesis/serializers.py +0 -328
  189. schemathesis/service/__init__.py +0 -18
  190. schemathesis/service/auth.py +0 -11
  191. schemathesis/service/ci.py +0 -202
  192. schemathesis/service/client.py +0 -133
  193. schemathesis/service/constants.py +0 -38
  194. schemathesis/service/events.py +0 -61
  195. schemathesis/service/extensions.py +0 -224
  196. schemathesis/service/hosts.py +0 -111
  197. schemathesis/service/metadata.py +0 -71
  198. schemathesis/service/models.py +0 -258
  199. schemathesis/service/report.py +0 -255
  200. schemathesis/service/serialization.py +0 -173
  201. schemathesis/service/usage.py +0 -66
  202. schemathesis/specs/graphql/loaders.py +0 -364
  203. schemathesis/specs/openapi/expressions/context.py +0 -16
  204. schemathesis/specs/openapi/loaders.py +0 -708
  205. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  206. schemathesis/specs/openapi/stateful/types.py +0 -14
  207. schemathesis/specs/openapi/validation.py +0 -26
  208. schemathesis/stateful/__init__.py +0 -147
  209. schemathesis/stateful/config.py +0 -97
  210. schemathesis/stateful/context.py +0 -135
  211. schemathesis/stateful/events.py +0 -274
  212. schemathesis/stateful/runner.py +0 -309
  213. schemathesis/stateful/sink.py +0 -68
  214. schemathesis/stateful/statistic.py +0 -22
  215. schemathesis/stateful/validation.py +0 -100
  216. schemathesis/targets.py +0 -77
  217. schemathesis/transports/__init__.py +0 -359
  218. schemathesis/transports/asgi.py +0 -7
  219. schemathesis/transports/auth.py +0 -38
  220. schemathesis/transports/headers.py +0 -36
  221. schemathesis/transports/responses.py +0 -57
  222. schemathesis/types.py +0 -44
  223. schemathesis/utils.py +0 -164
  224. schemathesis-3.39.7.dist-info/RECORD +0 -160
  225. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  226. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  227. /schemathesis/{internal → core}/result.py +0 -0
  228. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
  229. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -1,274 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import time
4
- from dataclasses import asdict as _asdict
5
- from dataclasses import dataclass
6
- from enum import Enum
7
- from typing import TYPE_CHECKING, Any
8
-
9
- from ..exceptions import format_exception
10
-
11
- if TYPE_CHECKING:
12
- from ..models import Case, Check
13
- from ..transports.responses import GenericResponse
14
- from .state_machine import APIStateMachine
15
-
16
-
17
- class RunStatus(str, Enum):
18
- """Status of the state machine run."""
19
-
20
- SUCCESS = "success"
21
- FAILURE = "failure"
22
- ERROR = "error"
23
- INTERRUPTED = "interrupted"
24
-
25
-
26
- @dataclass
27
- class StatefulEvent:
28
- """Basic stateful test event."""
29
-
30
- timestamp: float
31
-
32
- __slots__ = ("timestamp",)
33
-
34
- def asdict(self) -> dict[str, Any]:
35
- return _asdict(self)
36
-
37
-
38
- @dataclass
39
- class RunStarted(StatefulEvent):
40
- """Before executing all scenarios."""
41
-
42
- started_at: float
43
- state_machine: type[APIStateMachine]
44
-
45
- __slots__ = ("state_machine", "timestamp", "started_at")
46
-
47
- def __init__(self, *, state_machine: type[APIStateMachine]) -> None:
48
- self.state_machine = state_machine
49
- self.started_at = time.time()
50
- self.timestamp = time.monotonic()
51
-
52
- def asdict(self) -> dict[str, Any]:
53
- return {
54
- "timestamp": self.timestamp,
55
- "started_at": self.started_at,
56
- }
57
-
58
-
59
- @dataclass
60
- class RunFinished(StatefulEvent):
61
- """After executing all scenarios."""
62
-
63
- status: RunStatus
64
-
65
- __slots__ = ("timestamp", "status")
66
-
67
- def __init__(self, *, status: RunStatus) -> None:
68
- self.status = status
69
- self.timestamp = time.monotonic()
70
-
71
-
72
- class SuiteStatus(str, Enum):
73
- """Status of the suite execution."""
74
-
75
- SUCCESS = "success"
76
- FAILURE = "failure"
77
- ERROR = "error"
78
- INTERRUPTED = "interrupted"
79
-
80
-
81
- @dataclass
82
- class SuiteStarted(StatefulEvent):
83
- """Before executing a set of scenarios."""
84
-
85
- __slots__ = ("timestamp",)
86
-
87
- def __init__(self) -> None:
88
- self.timestamp = time.monotonic()
89
-
90
-
91
- @dataclass
92
- class SuiteFinished(StatefulEvent):
93
- """After executing a set of scenarios."""
94
-
95
- status: SuiteStatus
96
- failures: list[Check]
97
-
98
- __slots__ = ("timestamp", "status", "failures")
99
-
100
- def __init__(self, *, status: SuiteStatus, failures: list[Check]) -> None:
101
- self.status = status
102
- self.failures = failures
103
- self.timestamp = time.monotonic()
104
-
105
- def asdict(self) -> dict[str, Any]:
106
- from ..runner.serialization import SerializedCheck, _serialize_check
107
-
108
- return {
109
- "timestamp": self.timestamp,
110
- "status": self.status,
111
- "failures": [_serialize_check(SerializedCheck.from_check(failure)) for failure in self.failures],
112
- }
113
-
114
-
115
- class ScenarioStatus(str, Enum):
116
- """Status of a single scenario execution."""
117
-
118
- SUCCESS = "success"
119
- FAILURE = "failure"
120
- ERROR = "error"
121
- # Rejected by Hypothesis
122
- REJECTED = "rejected"
123
- INTERRUPTED = "interrupted"
124
-
125
-
126
- @dataclass
127
- class ScenarioStarted(StatefulEvent):
128
- """Before a single state machine execution."""
129
-
130
- # Whether this is a scenario that tries to reproduce a failure
131
- is_final: bool
132
-
133
- __slots__ = ("timestamp", "is_final")
134
-
135
- def __init__(self, *, is_final: bool) -> None:
136
- self.is_final = is_final
137
- self.timestamp = time.monotonic()
138
-
139
-
140
- @dataclass
141
- class ScenarioFinished(StatefulEvent):
142
- """After a single state machine execution."""
143
-
144
- status: ScenarioStatus
145
- # Whether this is a scenario that tries to reproduce a failure
146
- is_final: bool
147
-
148
- __slots__ = ("timestamp", "status", "is_final")
149
-
150
- def __init__(self, *, status: ScenarioStatus, is_final: bool) -> None:
151
- self.status = status
152
- self.is_final = is_final
153
- self.timestamp = time.monotonic()
154
-
155
-
156
- class StepStatus(str, Enum):
157
- """Status of a single state machine step."""
158
-
159
- SUCCESS = "success"
160
- FAILURE = "failure"
161
- ERROR = "error"
162
- INTERRUPTED = "interrupted"
163
-
164
-
165
- @dataclass
166
- class StepStarted(StatefulEvent):
167
- """Before a single state machine step."""
168
-
169
- __slots__ = ("timestamp",)
170
-
171
- def __init__(self) -> None:
172
- self.timestamp = time.monotonic()
173
-
174
-
175
- @dataclass
176
- class TransitionId:
177
- """Id of the the that was hit."""
178
-
179
- name: str
180
- # Status code as defined in the transition, i.e. may be `default`
181
- status_code: str
182
- source: str
183
-
184
- __slots__ = ("name", "status_code", "source")
185
-
186
-
187
- @dataclass
188
- class ResponseData:
189
- """Common data for responses."""
190
-
191
- status_code: int
192
- elapsed: float
193
- __slots__ = ("status_code", "elapsed")
194
-
195
-
196
- @dataclass
197
- class StepFinished(StatefulEvent):
198
- """After a single state machine step."""
199
-
200
- status: StepStatus | None
201
- transition_id: TransitionId | None
202
- target: str
203
- case: Case
204
- response: GenericResponse | None
205
- checks: list[Check]
206
-
207
- __slots__ = ("timestamp", "status", "transition_id", "target", "case", "response", "checks")
208
-
209
- def __init__(
210
- self,
211
- *,
212
- status: StepStatus | None,
213
- transition_id: TransitionId | None,
214
- target: str,
215
- case: Case,
216
- response: GenericResponse | None,
217
- checks: list[Check],
218
- ) -> None:
219
- self.status = status
220
- self.transition_id = transition_id
221
- self.target = target
222
- self.case = case
223
- self.response = response
224
- self.checks = checks
225
- self.timestamp = time.monotonic()
226
-
227
- def asdict(self) -> dict[str, Any]:
228
- return {
229
- "timestamp": self.timestamp,
230
- "status": self.status,
231
- "transition_id": {
232
- "name": self.transition_id.name,
233
- "status_code": self.transition_id.status_code,
234
- "source": self.transition_id.source,
235
- }
236
- if self.transition_id is not None
237
- else None,
238
- "target": self.target,
239
- "response": {
240
- "status_code": self.response.status_code,
241
- "elapsed": self.response.elapsed.total_seconds(),
242
- }
243
- if self.response is not None
244
- else None,
245
- }
246
-
247
-
248
- @dataclass
249
- class Interrupted(StatefulEvent):
250
- """The state machine execution was interrupted."""
251
-
252
- __slots__ = ("timestamp",)
253
-
254
- def __init__(self) -> None:
255
- self.timestamp = time.monotonic()
256
-
257
-
258
- @dataclass
259
- class Errored(StatefulEvent):
260
- """An error occurred during the state machine execution."""
261
-
262
- exception: Exception
263
-
264
- __slots__ = ("timestamp", "exception")
265
-
266
- def __init__(self, *, exception: Exception) -> None:
267
- self.exception = exception
268
- self.timestamp = time.monotonic()
269
-
270
- def asdict(self) -> dict[str, Any]:
271
- return {
272
- "timestamp": self.timestamp,
273
- "exception": format_exception(self.exception, True),
274
- }
@@ -1,309 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import queue
4
- import threading
5
- from contextlib import contextmanager
6
- from dataclasses import dataclass, field
7
- from typing import TYPE_CHECKING, Any, Generator, Iterator
8
-
9
- import hypothesis
10
- import requests
11
- from hypothesis.control import current_build_context
12
- from hypothesis.errors import Flaky, Unsatisfiable
13
-
14
- from ..exceptions import CheckFailed
15
- from ..internal.checks import CheckContext
16
- from ..targets import TargetMetricCollector
17
- from . import events
18
- from .config import StatefulTestRunnerConfig
19
- from .context import RunnerContext
20
- from .validation import validate_response
21
-
22
- if TYPE_CHECKING:
23
- from hypothesis.stateful import Rule
24
-
25
- from ..models import Case, CheckFunction
26
- from ..transports.responses import GenericResponse
27
- from .state_machine import APIStateMachine, Direction, StepResult
28
-
29
- EVENT_QUEUE_TIMEOUT = 0.01
30
-
31
-
32
- @dataclass
33
- class StatefulTestRunner:
34
- """Stateful test runner for the given state machine.
35
-
36
- By default, the test runner executes the state machine in a loop until there are no new failures are found.
37
- The loop is executed in a separate thread for better control over the execution and reporting.
38
- """
39
-
40
- # State machine class to use
41
- state_machine: type[APIStateMachine]
42
- # Test runner configuration that defines the runtime behavior
43
- config: StatefulTestRunnerConfig = field(default_factory=StatefulTestRunnerConfig)
44
- # Event to stop the execution
45
- stop_event: threading.Event = field(default_factory=threading.Event)
46
- # Queue to communicate with the state machine execution
47
- event_queue: queue.Queue = field(default_factory=queue.Queue)
48
-
49
- def execute(self) -> Iterator[events.StatefulEvent]:
50
- """Execute a test run for a state machine."""
51
- self.stop_event.clear()
52
-
53
- yield events.RunStarted(state_machine=self.state_machine)
54
-
55
- runner_thread = threading.Thread(
56
- target=_execute_state_machine_loop,
57
- kwargs={
58
- "state_machine": self.state_machine,
59
- "event_queue": self.event_queue,
60
- "config": self.config,
61
- "stop_event": self.stop_event,
62
- },
63
- )
64
- run_status = events.RunStatus.SUCCESS
65
-
66
- with thread_manager(runner_thread):
67
- try:
68
- while True:
69
- try:
70
- event = self.event_queue.get(timeout=EVENT_QUEUE_TIMEOUT)
71
- # Set the run status based on the suite status
72
- # ERROR & INTERRUPTED statuses are terminal, therefore they should not be overridden
73
- if isinstance(event, events.SuiteFinished):
74
- if event.status == events.SuiteStatus.FAILURE:
75
- run_status = events.RunStatus.FAILURE
76
- elif event.status == events.SuiteStatus.ERROR:
77
- run_status = events.RunStatus.ERROR
78
- elif event.status == events.SuiteStatus.INTERRUPTED:
79
- run_status = events.RunStatus.INTERRUPTED
80
- yield event
81
- except queue.Empty:
82
- if not runner_thread.is_alive():
83
- break
84
- except KeyboardInterrupt:
85
- # Immediately notify the runner thread to stop, even though that the event will be set below in `finally`
86
- self.stop()
87
- run_status = events.RunStatus.INTERRUPTED
88
- yield events.Interrupted()
89
- finally:
90
- self.stop()
91
-
92
- yield events.RunFinished(status=run_status)
93
-
94
- def stop(self) -> None:
95
- """Stop the execution of the state machine."""
96
- self.stop_event.set()
97
-
98
-
99
- @contextmanager
100
- def thread_manager(thread: threading.Thread) -> Generator[None, None, None]:
101
- thread.start()
102
- try:
103
- yield
104
- finally:
105
- thread.join()
106
-
107
-
108
- def _execute_state_machine_loop(
109
- *,
110
- state_machine: type[APIStateMachine],
111
- event_queue: queue.Queue,
112
- config: StatefulTestRunnerConfig,
113
- stop_event: threading.Event,
114
- ) -> None:
115
- """Execute the state machine testing loop."""
116
- from hypothesis import reporting
117
- from requests.structures import CaseInsensitiveDict
118
-
119
- from ..transports import RequestsTransport
120
-
121
- ctx = RunnerContext(metric_collector=TargetMetricCollector(targets=config.targets))
122
-
123
- call_kwargs: dict[str, Any] = {"headers": config.headers}
124
- if isinstance(state_machine.schema.transport, RequestsTransport):
125
- call_kwargs["timeout"] = config.request.prepared_timeout
126
- call_kwargs["verify"] = config.request.tls_verify
127
- call_kwargs["cert"] = config.request.cert
128
- if config.request.proxy is not None:
129
- call_kwargs["proxies"] = {"all": config.request.proxy}
130
- session = requests.Session()
131
- if config.auth is not None:
132
- session.auth = config.auth
133
- call_kwargs["session"] = session
134
- check_ctx = CheckContext(
135
- override=config.override,
136
- auth=config.auth,
137
- headers=CaseInsensitiveDict(config.headers) if config.headers else None,
138
- )
139
-
140
- class _InstrumentedStateMachine(state_machine): # type: ignore[valid-type,misc]
141
- """State machine with additional hooks for emitting events."""
142
-
143
- def setup(self) -> None:
144
- build_ctx = current_build_context()
145
- event_queue.put(events.ScenarioStarted(is_final=build_ctx.is_final))
146
- super().setup()
147
-
148
- def get_call_kwargs(self, case: Case) -> dict[str, Any]:
149
- return call_kwargs
150
-
151
- def _repr_step(self, rule: Rule, data: dict, result: StepResult) -> str:
152
- return ""
153
-
154
- if config.override is not None:
155
-
156
- def before_call(self, case: Case) -> None:
157
- for location, entry in config.override.for_operation(case.operation).items(): # type: ignore[union-attr]
158
- if entry:
159
- container = getattr(case, location) or {}
160
- container.update(entry)
161
- setattr(case, location, container)
162
- return super().before_call(case)
163
-
164
- def step(self, case: Case, previous: tuple[StepResult, Direction] | None = None) -> StepResult | None:
165
- # Checking the stop event once inside `step` is sufficient as it is called frequently
166
- # The idea is to stop the execution as soon as possible
167
- if stop_event.is_set():
168
- raise KeyboardInterrupt
169
- event_queue.put(events.StepStarted())
170
- try:
171
- if config.dry_run:
172
- return None
173
- if config.unique_data:
174
- cached = ctx.get_step_outcome(case)
175
- if isinstance(cached, BaseException):
176
- raise cached
177
- elif cached is None:
178
- return None
179
- result = super().step(case, previous)
180
- ctx.step_succeeded()
181
- except CheckFailed as exc:
182
- if config.unique_data:
183
- ctx.store_step_outcome(case, exc)
184
- ctx.step_failed()
185
- raise
186
- except Exception as exc:
187
- if config.unique_data:
188
- ctx.store_step_outcome(case, exc)
189
- ctx.step_errored()
190
- raise
191
- except KeyboardInterrupt:
192
- ctx.step_interrupted()
193
- raise
194
- except BaseException as exc:
195
- if config.unique_data:
196
- ctx.store_step_outcome(case, exc)
197
- raise exc
198
- else:
199
- if config.unique_data:
200
- ctx.store_step_outcome(case, None)
201
- finally:
202
- transition_id: events.TransitionId | None
203
- if previous is not None:
204
- transition = previous[1]
205
- transition_id = events.TransitionId(
206
- name=transition.name,
207
- status_code=transition.status_code,
208
- source=transition.operation.verbose_name,
209
- )
210
- else:
211
- transition_id = None
212
- event_queue.put(
213
- events.StepFinished(
214
- status=ctx.current_step_status,
215
- transition_id=transition_id,
216
- target=case.operation.verbose_name,
217
- case=case,
218
- response=ctx.current_response,
219
- checks=ctx.checks_for_step,
220
- )
221
- )
222
- ctx.reset_step()
223
- return result
224
-
225
- def validate_response(
226
- self, response: GenericResponse, case: Case, additional_checks: tuple[CheckFunction, ...] = ()
227
- ) -> None:
228
- ctx.collect_metric(case, response)
229
- ctx.current_response = response
230
- validate_response(
231
- response=response,
232
- case=case,
233
- runner_ctx=ctx,
234
- check_ctx=check_ctx,
235
- checks=config.checks,
236
- additional_checks=additional_checks,
237
- max_response_time=config.max_response_time,
238
- )
239
-
240
- def teardown(self) -> None:
241
- build_ctx = current_build_context()
242
- event_queue.put(
243
- events.ScenarioFinished(
244
- status=ctx.current_scenario_status,
245
- is_final=build_ctx.is_final,
246
- )
247
- )
248
- ctx.maximize_metrics()
249
- ctx.reset_scenario()
250
- super().teardown()
251
-
252
- if config.seed is not None:
253
- InstrumentedStateMachine = hypothesis.seed(config.seed)(_InstrumentedStateMachine)
254
- else:
255
- InstrumentedStateMachine = _InstrumentedStateMachine
256
-
257
- def should_stop() -> bool:
258
- return config.exit_first or (config.max_failures is not None and ctx.failures_count >= config.max_failures)
259
-
260
- while True:
261
- # This loop is running until no new failures are found in a single iteration
262
- event_queue.put(events.SuiteStarted())
263
- if stop_event.is_set():
264
- event_queue.put(events.SuiteFinished(status=events.SuiteStatus.INTERRUPTED, failures=[]))
265
- break
266
- suite_status = events.SuiteStatus.SUCCESS
267
- try:
268
- with reporting.with_reporter(lambda _: None): # type: ignore
269
- InstrumentedStateMachine.run(settings=config.hypothesis_settings)
270
- except KeyboardInterrupt:
271
- # Raised in the state machine when the stop event is set or it is raised by the user's code
272
- # that is placed in the base class of the state machine.
273
- # Therefore, set the stop event to cover the latter case
274
- stop_event.set()
275
- suite_status = events.SuiteStatus.INTERRUPTED
276
- break
277
- except CheckFailed as exc:
278
- # When a check fails, the state machine is stopped
279
- # The failure is already sent to the queue by the state machine
280
- # Here we need to either exit or re-run the state machine with this failure marked as known
281
- suite_status = events.SuiteStatus.FAILURE
282
- if should_stop():
283
- break
284
- ctx.mark_as_seen_in_run(exc)
285
- continue
286
- except Flaky:
287
- suite_status = events.SuiteStatus.FAILURE
288
- if should_stop():
289
- break
290
- # Mark all failures in this suite as seen to prevent them being re-discovered
291
- ctx.mark_current_suite_as_seen_in_run()
292
- continue
293
- except Exception as exc:
294
- if isinstance(exc, Unsatisfiable) and ctx.completed_scenarios > 0:
295
- # Sometimes Hypothesis randomly gives up on generating some complex cases. However, if we know that
296
- # values are possible to generate based on the previous observations, we retry the generation
297
- if ctx.completed_scenarios >= config.hypothesis_settings.max_examples:
298
- # Avoid infinite restarts
299
- break
300
- continue
301
- # Any other exception is an inner error and the test run should be stopped
302
- suite_status = events.SuiteStatus.ERROR
303
- event_queue.put(events.Errored(exception=exc))
304
- break
305
- finally:
306
- event_queue.put(events.SuiteFinished(status=suite_status, failures=ctx.failures_for_suite))
307
- ctx.reset()
308
- # Exit on the first successful state machine execution
309
- break
@@ -1,68 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass, field
4
- from typing import TYPE_CHECKING
5
-
6
- from . import events
7
-
8
- if TYPE_CHECKING:
9
- from ..models import Check
10
- from .statistic import TransitionStats
11
-
12
-
13
- @dataclass
14
- class AverageResponseTime:
15
- """Average response time for a given status code.
16
-
17
- Stored as a sum of all response times and a count of responses.
18
- """
19
-
20
- total: float
21
- count: int
22
-
23
- __slots__ = ("total", "count")
24
-
25
- def __init__(self) -> None:
26
- self.total = 0.0
27
- self.count = 0
28
-
29
-
30
- @dataclass
31
- class StateMachineSink:
32
- """Collects events and stores data about the state machine execution."""
33
-
34
- transitions: TransitionStats
35
- response_times: dict[str, dict[int, AverageResponseTime]] = field(default_factory=dict)
36
- steps: dict[events.StepStatus, int] = field(default_factory=lambda: {status: 0 for status in events.StepStatus})
37
- scenarios: dict[events.ScenarioStatus, int] = field(
38
- default_factory=lambda: {status: 0 for status in events.ScenarioStatus}
39
- )
40
- suites: dict[events.SuiteStatus, int] = field(default_factory=lambda: {status: 0 for status in events.SuiteStatus})
41
- failures: list[Check] = field(default_factory=list)
42
- start_time: float | None = None
43
- end_time: float | None = None
44
-
45
- def consume(self, event: events.StatefulEvent) -> None:
46
- self.transitions.consume(event)
47
- if isinstance(event, events.RunStarted):
48
- self.start_time = event.timestamp
49
- elif isinstance(event, events.StepFinished) and event.status is not None:
50
- self.steps[event.status] += 1
51
- responses = self.response_times.setdefault(event.target, {})
52
- if event.response is not None:
53
- average = responses.setdefault(event.response.status_code, AverageResponseTime())
54
- average.total += event.response.elapsed.total_seconds()
55
- average.count += 1
56
- elif isinstance(event, events.ScenarioFinished):
57
- self.scenarios[event.status] += 1
58
- elif isinstance(event, events.SuiteFinished):
59
- self.suites[event.status] += 1
60
- self.failures.extend(event.failures)
61
- elif isinstance(event, events.RunFinished):
62
- self.end_time = event.timestamp
63
-
64
- @property
65
- def duration(self) -> float | None:
66
- if self.start_time is not None and self.end_time is not None:
67
- return self.end_time - self.start_time
68
- return None
@@ -1,22 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from typing import TYPE_CHECKING
5
-
6
- if TYPE_CHECKING:
7
- from . import events
8
-
9
-
10
- @dataclass
11
- class TransitionStats:
12
- """Statistic for transitions in a state machine."""
13
-
14
- def consume(self, event: events.StatefulEvent) -> None:
15
- raise NotImplementedError
16
-
17
- def copy(self) -> TransitionStats:
18
- """Create a copy of the statistic."""
19
- raise NotImplementedError
20
-
21
- def to_formatted_table(self, width: int) -> str:
22
- raise NotImplementedError