schemathesis 3.39.16__py3-none-any.whl → 4.0.0__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.
- schemathesis/__init__.py +41 -79
- schemathesis/auths.py +111 -122
- schemathesis/checks.py +169 -60
- schemathesis/cli/__init__.py +15 -2117
- schemathesis/cli/commands/__init__.py +85 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +590 -0
- schemathesis/cli/commands/run/context.py +204 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
- schemathesis/cli/commands/run/handlers/output.py +1628 -0
- schemathesis/cli/commands/run/loaders.py +114 -0
- schemathesis/cli/commands/run/validation.py +246 -0
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +84 -0
- schemathesis/cli/{options.py → ext/options.py} +36 -34
- schemathesis/config/__init__.py +189 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +99 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +156 -0
- schemathesis/config/_generation.py +149 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +327 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +187 -0
- schemathesis/config/_projects.py +527 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +885 -0
- schemathesis/core/__init__.py +67 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +459 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/{throttling.py → core/rate_limit.py} +16 -17
- schemathesis/core/registries.py +31 -0
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +54 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +118 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +169 -0
- schemathesis/engine/errors.py +464 -0
- schemathesis/engine/events.py +258 -0
- schemathesis/engine/phases/__init__.py +88 -0
- schemathesis/{runner → engine/phases}/probes.py +52 -68
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +356 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +212 -0
- schemathesis/engine/phases/unit/_executor.py +416 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +247 -0
- schemathesis/errors.py +43 -0
- schemathesis/filters.py +17 -98
- schemathesis/generation/__init__.py +5 -33
- schemathesis/generation/case.py +317 -0
- schemathesis/generation/coverage.py +282 -175
- schemathesis/generation/hypothesis/__init__.py +36 -0
- schemathesis/generation/hypothesis/builder.py +800 -0
- schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +116 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +278 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +284 -0
- schemathesis/hooks.py +80 -101
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +455 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +313 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +281 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -273
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +42 -6
- schemathesis/specs/graphql/schemas.py +141 -137
- schemathesis/specs/graphql/validation.py +11 -17
- schemathesis/specs/openapi/__init__.py +6 -1
- schemathesis/specs/openapi/_cache.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +142 -156
- schemathesis/specs/openapi/checks.py +368 -257
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +23 -21
- schemathesis/specs/openapi/expressions/__init__.py +31 -19
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/lexer.py +1 -1
- schemathesis/specs/openapi/expressions/nodes.py +36 -41
- schemathesis/specs/openapi/expressions/parser.py +1 -1
- schemathesis/specs/openapi/formats.py +35 -7
- schemathesis/specs/openapi/media_types.py +53 -12
- schemathesis/specs/openapi/negative/__init__.py +7 -4
- schemathesis/specs/openapi/negative/mutations.py +6 -5
- schemathesis/specs/openapi/parameters.py +7 -10
- schemathesis/specs/openapi/patterns.py +94 -31
- schemathesis/specs/openapi/references.py +12 -53
- schemathesis/specs/openapi/schemas.py +233 -307
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +12 -6
- schemathesis/specs/openapi/stateful/__init__.py +268 -133
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/links.py +209 -0
- schemathesis/transport/__init__.py +142 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +124 -0
- schemathesis/transport/requests.py +244 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -11
- schemathesis/transport/wsgi.py +171 -0
- schemathesis-4.0.0.dist-info/METADATA +204 -0
- schemathesis-4.0.0.dist-info/RECORD +164 -0
- {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
- {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -717
- schemathesis/_override.py +0 -50
- schemathesis/_patches.py +0 -21
- schemathesis/_rate_limiter.py +0 -7
- schemathesis/cli/callbacks.py +0 -466
- schemathesis/cli/cassettes.py +0 -561
- schemathesis/cli/context.py +0 -75
- schemathesis/cli/debug.py +0 -27
- schemathesis/cli/handlers.py +0 -19
- schemathesis/cli/junitxml.py +0 -124
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -920
- schemathesis/cli/output/short.py +0 -59
- schemathesis/cli/reporting.py +0 -79
- schemathesis/cli/sanitization.py +0 -26
- schemathesis/code_samples.py +0 -151
- schemathesis/constants.py +0 -54
- schemathesis/contrib/__init__.py +0 -11
- schemathesis/contrib/openapi/__init__.py +0 -11
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -16
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -571
- schemathesis/experimental/__init__.py +0 -109
- schemathesis/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -284
- schemathesis/fixups/__init__.py +0 -37
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -28
- schemathesis/generation/_methods.py +0 -44
- schemathesis/graphql.py +0 -3
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/checks.py +0 -86
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -37
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- schemathesis/internal/output.py +0 -68
- schemathesis/internal/transformation.py +0 -26
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -474
- schemathesis/loaders.py +0 -122
- schemathesis/models.py +0 -1341
- schemathesis/parameters.py +0 -90
- schemathesis/runner/__init__.py +0 -605
- schemathesis/runner/events.py +0 -389
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/context.py +0 -88
- schemathesis/runner/impl/core.py +0 -1280
- schemathesis/runner/impl/solo.py +0 -80
- schemathesis/runner/impl/threadpool.py +0 -391
- schemathesis/runner/serialization.py +0 -544
- schemathesis/sanitization.py +0 -252
- schemathesis/serializers.py +0 -328
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -202
- schemathesis/service/client.py +0 -133
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -61
- schemathesis/service/extensions.py +0 -224
- schemathesis/service/hosts.py +0 -111
- schemathesis/service/metadata.py +0 -71
- schemathesis/service/models.py +0 -258
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -173
- schemathesis/service/usage.py +0 -66
- schemathesis/specs/graphql/loaders.py +0 -364
- schemathesis/specs/openapi/expressions/context.py +0 -16
- schemathesis/specs/openapi/links.py +0 -389
- schemathesis/specs/openapi/loaders.py +0 -707
- schemathesis/specs/openapi/stateful/statistic.py +0 -198
- schemathesis/specs/openapi/stateful/types.py +0 -14
- schemathesis/specs/openapi/validation.py +0 -26
- schemathesis/stateful/__init__.py +0 -147
- schemathesis/stateful/config.py +0 -97
- schemathesis/stateful/context.py +0 -135
- schemathesis/stateful/events.py +0 -274
- schemathesis/stateful/runner.py +0 -309
- schemathesis/stateful/sink.py +0 -68
- schemathesis/stateful/state_machine.py +0 -328
- schemathesis/stateful/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -369
- schemathesis/transports/asgi.py +0 -7
- schemathesis/transports/auth.py +0 -38
- schemathesis/transports/headers.py +0 -36
- schemathesis/transports/responses.py +0 -57
- schemathesis/types.py +0 -44
- schemathesis/utils.py +0 -164
- schemathesis-3.39.16.dist-info/METADATA +0 -293
- schemathesis-3.39.16.dist-info/RECORD +0 -160
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
- /schemathesis/{internal → core}/result.py +0 -0
- {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
schemathesis/stateful/events.py
DELETED
@@ -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
|
-
}
|
schemathesis/stateful/runner.py
DELETED
@@ -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
|
schemathesis/stateful/sink.py
DELETED
@@ -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
|