schemathesis 3.39.15__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 +238 -308
- 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.15.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
- {schemathesis-3.39.15.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 -712
- 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.15.dist-info/METADATA +0 -293
- schemathesis-3.39.15.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.15.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,258 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import time
|
4
|
+
import uuid
|
5
|
+
from dataclasses import dataclass
|
6
|
+
from typing import TYPE_CHECKING, Generator
|
7
|
+
|
8
|
+
from schemathesis.core.result import Result
|
9
|
+
from schemathesis.engine.errors import EngineErrorInfo
|
10
|
+
from schemathesis.engine.phases import Phase, PhaseName
|
11
|
+
from schemathesis.engine.recorder import ScenarioRecorder
|
12
|
+
|
13
|
+
if TYPE_CHECKING:
|
14
|
+
from schemathesis.engine import Status
|
15
|
+
from schemathesis.engine.phases.probes import ProbePayload
|
16
|
+
|
17
|
+
EventGenerator = Generator["EngineEvent", None, None]
|
18
|
+
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
class EngineEvent:
|
22
|
+
"""An event within the engine's lifecycle."""
|
23
|
+
|
24
|
+
id: uuid.UUID
|
25
|
+
timestamp: float
|
26
|
+
# Indicates whether this event is the last in the event stream
|
27
|
+
is_terminal = False
|
28
|
+
|
29
|
+
|
30
|
+
@dataclass
|
31
|
+
class EngineStarted(EngineEvent):
|
32
|
+
"""Start of an engine."""
|
33
|
+
|
34
|
+
__slots__ = ("id", "timestamp")
|
35
|
+
|
36
|
+
def __init__(self) -> None:
|
37
|
+
self.id = uuid.uuid4()
|
38
|
+
self.timestamp = time.time()
|
39
|
+
|
40
|
+
|
41
|
+
@dataclass
|
42
|
+
class PhaseEvent(EngineEvent):
|
43
|
+
"""Event associated with a specific execution phase."""
|
44
|
+
|
45
|
+
phase: Phase
|
46
|
+
|
47
|
+
|
48
|
+
@dataclass
|
49
|
+
class PhaseStarted(PhaseEvent):
|
50
|
+
"""Start of an execution phase."""
|
51
|
+
|
52
|
+
__slots__ = ("id", "timestamp", "phase")
|
53
|
+
|
54
|
+
def __init__(self, *, phase: Phase) -> None:
|
55
|
+
self.id = uuid.uuid4()
|
56
|
+
self.timestamp = time.time()
|
57
|
+
self.phase = phase
|
58
|
+
|
59
|
+
|
60
|
+
@dataclass
|
61
|
+
class PhaseFinished(PhaseEvent):
|
62
|
+
"""End of an execution phase."""
|
63
|
+
|
64
|
+
status: Status
|
65
|
+
payload: Result[ProbePayload, Exception] | None
|
66
|
+
|
67
|
+
__slots__ = ("id", "timestamp", "phase", "status", "payload")
|
68
|
+
|
69
|
+
def __init__(self, *, phase: Phase, status: Status, payload: Result[ProbePayload, Exception] | None) -> None:
|
70
|
+
self.id = uuid.uuid4()
|
71
|
+
self.timestamp = time.time()
|
72
|
+
self.phase = phase
|
73
|
+
self.status = status
|
74
|
+
self.payload = payload
|
75
|
+
|
76
|
+
|
77
|
+
@dataclass
|
78
|
+
class TestEvent(EngineEvent):
|
79
|
+
phase: PhaseName
|
80
|
+
|
81
|
+
|
82
|
+
@dataclass
|
83
|
+
class SuiteStarted(TestEvent):
|
84
|
+
"""Before executing a set of scenarios."""
|
85
|
+
|
86
|
+
__slots__ = ("id", "timestamp", "phase")
|
87
|
+
|
88
|
+
def __init__(self, *, phase: PhaseName) -> None:
|
89
|
+
self.id = uuid.uuid4()
|
90
|
+
self.timestamp = time.time()
|
91
|
+
self.phase = phase
|
92
|
+
|
93
|
+
|
94
|
+
@dataclass
|
95
|
+
class SuiteFinished(TestEvent):
|
96
|
+
"""After executing a set of test scenarios."""
|
97
|
+
|
98
|
+
status: Status
|
99
|
+
|
100
|
+
__slots__ = ("id", "timestamp", "phase", "status")
|
101
|
+
|
102
|
+
def __init__(self, *, id: uuid.UUID, phase: PhaseName, status: Status) -> None:
|
103
|
+
self.id = id
|
104
|
+
self.timestamp = time.time()
|
105
|
+
self.phase = phase
|
106
|
+
self.status = status
|
107
|
+
|
108
|
+
|
109
|
+
@dataclass
|
110
|
+
class ScenarioEvent(TestEvent):
|
111
|
+
suite_id: uuid.UUID
|
112
|
+
|
113
|
+
|
114
|
+
@dataclass
|
115
|
+
class ScenarioStarted(ScenarioEvent):
|
116
|
+
"""Before executing a grouped set of test steps."""
|
117
|
+
|
118
|
+
__slots__ = ("id", "timestamp", "phase", "suite_id", "label")
|
119
|
+
|
120
|
+
def __init__(self, *, phase: PhaseName, suite_id: uuid.UUID, label: str | None) -> None:
|
121
|
+
self.id = uuid.uuid4()
|
122
|
+
self.timestamp = time.time()
|
123
|
+
self.phase = phase
|
124
|
+
self.suite_id = suite_id
|
125
|
+
self.label = label
|
126
|
+
|
127
|
+
|
128
|
+
@dataclass
|
129
|
+
class ScenarioFinished(ScenarioEvent):
|
130
|
+
"""After executing a grouped set of test steps."""
|
131
|
+
|
132
|
+
status: Status
|
133
|
+
recorder: ScenarioRecorder
|
134
|
+
elapsed_time: float
|
135
|
+
skip_reason: str | None
|
136
|
+
# Whether this is a scenario that tries to reproduce a failure
|
137
|
+
is_final: bool
|
138
|
+
|
139
|
+
__slots__ = (
|
140
|
+
"id",
|
141
|
+
"timestamp",
|
142
|
+
"phase",
|
143
|
+
"suite_id",
|
144
|
+
"label",
|
145
|
+
"status",
|
146
|
+
"recorder",
|
147
|
+
"elapsed_time",
|
148
|
+
"skip_reason",
|
149
|
+
"is_final",
|
150
|
+
)
|
151
|
+
|
152
|
+
def __init__(
|
153
|
+
self,
|
154
|
+
*,
|
155
|
+
id: uuid.UUID,
|
156
|
+
phase: PhaseName,
|
157
|
+
suite_id: uuid.UUID,
|
158
|
+
label: str | None,
|
159
|
+
status: Status,
|
160
|
+
recorder: ScenarioRecorder,
|
161
|
+
elapsed_time: float,
|
162
|
+
skip_reason: str | None,
|
163
|
+
is_final: bool,
|
164
|
+
) -> None:
|
165
|
+
self.id = id
|
166
|
+
self.timestamp = time.time()
|
167
|
+
self.phase = phase
|
168
|
+
self.suite_id = suite_id
|
169
|
+
self.label = label
|
170
|
+
self.status = status
|
171
|
+
self.recorder = recorder
|
172
|
+
self.elapsed_time = elapsed_time
|
173
|
+
self.skip_reason = skip_reason
|
174
|
+
self.is_final = is_final
|
175
|
+
|
176
|
+
|
177
|
+
@dataclass
|
178
|
+
class Interrupted(EngineEvent):
|
179
|
+
"""If execution was interrupted by Ctrl-C, or a received SIGTERM."""
|
180
|
+
|
181
|
+
phase: PhaseName | None
|
182
|
+
|
183
|
+
__slots__ = ("id", "timestamp", "phase")
|
184
|
+
|
185
|
+
def __init__(self, *, phase: PhaseName | None) -> None:
|
186
|
+
self.id = uuid.uuid4()
|
187
|
+
self.timestamp = time.time()
|
188
|
+
self.phase = phase
|
189
|
+
|
190
|
+
|
191
|
+
@dataclass
|
192
|
+
class NonFatalError(EngineEvent):
|
193
|
+
"""Error that doesn't halt execution but should be reported."""
|
194
|
+
|
195
|
+
info: EngineErrorInfo
|
196
|
+
value: Exception
|
197
|
+
phase: PhaseName
|
198
|
+
label: str
|
199
|
+
related_to_operation: bool
|
200
|
+
|
201
|
+
__slots__ = ("id", "timestamp", "info", "value", "phase", "label", "related_to_operation")
|
202
|
+
|
203
|
+
def __init__(
|
204
|
+
self,
|
205
|
+
*,
|
206
|
+
error: Exception,
|
207
|
+
phase: PhaseName,
|
208
|
+
label: str,
|
209
|
+
related_to_operation: bool,
|
210
|
+
code_sample: str | None = None,
|
211
|
+
) -> None:
|
212
|
+
self.id = uuid.uuid4()
|
213
|
+
self.timestamp = time.time()
|
214
|
+
self.info = EngineErrorInfo(error=error, code_sample=code_sample)
|
215
|
+
self.value = error
|
216
|
+
self.phase = phase
|
217
|
+
self.label = label
|
218
|
+
self.related_to_operation = related_to_operation
|
219
|
+
|
220
|
+
def __eq__(self, other: object) -> bool:
|
221
|
+
assert isinstance(other, NonFatalError)
|
222
|
+
return self.label == other.label and type(self.value) is type(other.value)
|
223
|
+
|
224
|
+
def __hash__(self) -> int:
|
225
|
+
return hash((self.label, type(self.value)))
|
226
|
+
|
227
|
+
|
228
|
+
@dataclass
|
229
|
+
class FatalError(EngineEvent):
|
230
|
+
"""Internal error in the engine."""
|
231
|
+
|
232
|
+
exception: Exception
|
233
|
+
is_terminal = True
|
234
|
+
|
235
|
+
__slots__ = ("id", "timestamp", "exception")
|
236
|
+
|
237
|
+
def __init__(self, *, exception: Exception) -> None:
|
238
|
+
self.id = uuid.uuid4()
|
239
|
+
self.timestamp = time.time()
|
240
|
+
self.exception = exception
|
241
|
+
|
242
|
+
|
243
|
+
@dataclass
|
244
|
+
class EngineFinished(EngineEvent):
|
245
|
+
"""The final event of the run.
|
246
|
+
|
247
|
+
No more events after this point.
|
248
|
+
"""
|
249
|
+
|
250
|
+
is_terminal = True
|
251
|
+
running_time: float
|
252
|
+
|
253
|
+
__slots__ = ("id", "timestamp", "running_time")
|
254
|
+
|
255
|
+
def __init__(self, *, running_time: float) -> None:
|
256
|
+
self.id = uuid.uuid4()
|
257
|
+
self.timestamp = time.time()
|
258
|
+
self.running_time = running_time
|
@@ -0,0 +1,88 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import enum
|
4
|
+
import warnings
|
5
|
+
from dataclasses import dataclass
|
6
|
+
from typing import TYPE_CHECKING
|
7
|
+
|
8
|
+
if TYPE_CHECKING:
|
9
|
+
from schemathesis.engine.context import EngineContext
|
10
|
+
from schemathesis.engine.events import EventGenerator
|
11
|
+
|
12
|
+
|
13
|
+
class PhaseName(str, enum.Enum):
|
14
|
+
"""Available execution phases."""
|
15
|
+
|
16
|
+
PROBING = "API probing"
|
17
|
+
EXAMPLES = "Examples"
|
18
|
+
COVERAGE = "Coverage"
|
19
|
+
FUZZING = "Fuzzing"
|
20
|
+
STATEFUL_TESTING = "Stateful"
|
21
|
+
|
22
|
+
@classmethod
|
23
|
+
def defaults(cls) -> list[PhaseName]:
|
24
|
+
return [PhaseName.EXAMPLES, PhaseName.COVERAGE, PhaseName.FUZZING, PhaseName.STATEFUL_TESTING]
|
25
|
+
|
26
|
+
@property
|
27
|
+
def name(self) -> str:
|
28
|
+
return {
|
29
|
+
PhaseName.PROBING: "probing",
|
30
|
+
PhaseName.EXAMPLES: "examples",
|
31
|
+
PhaseName.COVERAGE: "coverage",
|
32
|
+
PhaseName.FUZZING: "fuzzing",
|
33
|
+
PhaseName.STATEFUL_TESTING: "stateful",
|
34
|
+
}[self]
|
35
|
+
|
36
|
+
@classmethod
|
37
|
+
def from_str(cls, value: str) -> PhaseName:
|
38
|
+
return {
|
39
|
+
"probing": cls.PROBING,
|
40
|
+
"examples": cls.EXAMPLES,
|
41
|
+
"coverage": cls.COVERAGE,
|
42
|
+
"fuzzing": cls.FUZZING,
|
43
|
+
"stateful": cls.STATEFUL_TESTING,
|
44
|
+
}[value.lower()]
|
45
|
+
|
46
|
+
|
47
|
+
class PhaseSkipReason(str, enum.Enum):
|
48
|
+
"""Reasons why a phase might not be executed."""
|
49
|
+
|
50
|
+
DISABLED = "disabled" # Explicitly disabled via config
|
51
|
+
NOT_SUPPORTED = "not supported" # Feature not supported by schema
|
52
|
+
NOT_APPLICABLE = "not applicable" # No relevant data (e.g., no links for stateful)
|
53
|
+
FAILURE_LIMIT_REACHED = "failure limit reached"
|
54
|
+
NOTHING_TO_TEST = "nothing to test"
|
55
|
+
|
56
|
+
|
57
|
+
@dataclass
|
58
|
+
class Phase:
|
59
|
+
"""A logically separate engine execution phase."""
|
60
|
+
|
61
|
+
name: PhaseName
|
62
|
+
is_supported: bool
|
63
|
+
is_enabled: bool = True
|
64
|
+
skip_reason: PhaseSkipReason | None = None
|
65
|
+
|
66
|
+
def should_execute(self, ctx: EngineContext) -> bool:
|
67
|
+
"""Determine if phase should run based on context & configuration."""
|
68
|
+
return self.is_enabled and not ctx.has_to_stop
|
69
|
+
|
70
|
+
|
71
|
+
def execute(ctx: EngineContext, phase: Phase) -> EventGenerator:
|
72
|
+
from urllib3.exceptions import InsecureRequestWarning
|
73
|
+
|
74
|
+
from . import probes, stateful, unit
|
75
|
+
|
76
|
+
with warnings.catch_warnings():
|
77
|
+
warnings.simplefilter("ignore", InsecureRequestWarning)
|
78
|
+
|
79
|
+
if phase.name == PhaseName.PROBING:
|
80
|
+
yield from probes.execute(ctx, phase)
|
81
|
+
elif phase.name == PhaseName.EXAMPLES:
|
82
|
+
yield from unit.execute(ctx, phase)
|
83
|
+
elif phase.name == PhaseName.COVERAGE:
|
84
|
+
yield from unit.execute(ctx, phase)
|
85
|
+
elif phase.name == PhaseName.FUZZING:
|
86
|
+
yield from unit.execute(ctx, phase)
|
87
|
+
elif phase.name == PhaseName.STATEFUL_TESTING:
|
88
|
+
yield from stateful.execute(ctx, phase)
|
@@ -10,32 +10,56 @@ from __future__ import annotations
|
|
10
10
|
|
11
11
|
import enum
|
12
12
|
import warnings
|
13
|
-
from dataclasses import
|
14
|
-
from typing import TYPE_CHECKING
|
13
|
+
from dataclasses import dataclass
|
14
|
+
from typing import TYPE_CHECKING
|
15
15
|
|
16
|
-
from
|
17
|
-
from
|
18
|
-
from
|
19
|
-
from
|
20
|
-
from ..transports import RequestConfig
|
21
|
-
from ..transports.auth import get_requests_auth
|
16
|
+
from schemathesis.core.result import Err, Ok, Result
|
17
|
+
from schemathesis.core.transport import USER_AGENT
|
18
|
+
from schemathesis.engine import Status, events
|
19
|
+
from schemathesis.transport.prepare import get_default_headers
|
22
20
|
|
23
21
|
if TYPE_CHECKING:
|
24
22
|
import requests
|
25
23
|
|
26
|
-
from
|
24
|
+
from schemathesis.engine.context import EngineContext
|
25
|
+
from schemathesis.engine.events import EventGenerator
|
26
|
+
from schemathesis.engine.phases import Phase
|
27
|
+
from schemathesis.schemas import BaseSchema
|
27
28
|
|
28
29
|
|
29
|
-
|
30
|
+
@dataclass
|
31
|
+
class ProbePayload:
|
32
|
+
probes: list[ProbeRun]
|
33
|
+
|
34
|
+
__slots__ = ("probes",)
|
35
|
+
|
36
|
+
|
37
|
+
def execute(ctx: EngineContext, phase: Phase) -> EventGenerator:
|
38
|
+
"""Discover capabilities of the tested app."""
|
39
|
+
probes = run(ctx)
|
40
|
+
status = Status.SUCCESS
|
41
|
+
payload: Result[ProbePayload, Exception] | None = None
|
42
|
+
for result in probes:
|
43
|
+
if isinstance(result.probe, NullByteInHeader) and result.is_failure:
|
44
|
+
from ...specs.openapi import formats
|
45
|
+
from ...specs.openapi.formats import HEADER_FORMAT, header_values
|
46
|
+
|
47
|
+
formats.register(HEADER_FORMAT, header_values(exclude_characters="\n\r\x00"))
|
48
|
+
if result.error is not None:
|
49
|
+
status = Status.ERROR
|
50
|
+
payload = Err(result.error)
|
51
|
+
else:
|
52
|
+
status = Status.SUCCESS
|
53
|
+
payload = Ok(ProbePayload(probes=probes))
|
54
|
+
yield events.PhaseFinished(phase=phase, status=status, payload=payload)
|
30
55
|
|
31
56
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
headers: dict[str, str] | None = None
|
57
|
+
def run(ctx: EngineContext) -> list[ProbeRun]:
|
58
|
+
"""Run all probes against the given schema."""
|
59
|
+
return [send(probe(), ctx) for probe in PROBES]
|
60
|
+
|
61
|
+
|
62
|
+
HEADER_NAME = "X-Schemathesis-Probe"
|
39
63
|
|
40
64
|
|
41
65
|
@dataclass
|
@@ -45,7 +69,7 @@ class Probe:
|
|
45
69
|
name: str
|
46
70
|
|
47
71
|
def prepare_request(
|
48
|
-
self, session: requests.Session, request: requests.Request, schema: BaseSchema
|
72
|
+
self, session: requests.Session, request: requests.Request, schema: BaseSchema
|
49
73
|
) -> requests.PreparedRequest:
|
50
74
|
raise NotImplementedError
|
51
75
|
|
@@ -70,49 +94,24 @@ class ProbeRun:
|
|
70
94
|
outcome: ProbeOutcome
|
71
95
|
request: requests.PreparedRequest | None = None
|
72
96
|
response: requests.Response | None = None
|
73
|
-
error:
|
97
|
+
error: Exception | None = None
|
74
98
|
|
75
99
|
@property
|
76
100
|
def is_failure(self) -> bool:
|
77
101
|
return self.outcome == ProbeOutcome.FAILURE
|
78
102
|
|
79
|
-
def serialize(self) -> dict[str, Any]:
|
80
|
-
"""Serialize probe results so it can be sent over the network."""
|
81
|
-
if self.request:
|
82
|
-
_request = Request.from_prepared_request(self.request)
|
83
|
-
sanitize_request(_request)
|
84
|
-
request = asdict(_request)
|
85
|
-
else:
|
86
|
-
request = None
|
87
|
-
if self.response:
|
88
|
-
sanitize_response(self.response)
|
89
|
-
response = asdict(Response.from_requests(self.response))
|
90
|
-
else:
|
91
|
-
response = None
|
92
|
-
if self.error:
|
93
|
-
error = format_exception(self.error)
|
94
|
-
else:
|
95
|
-
error = None
|
96
|
-
return {
|
97
|
-
"name": self.probe.name,
|
98
|
-
"outcome": self.outcome.value,
|
99
|
-
"request": request,
|
100
|
-
"response": response,
|
101
|
-
"error": error,
|
102
|
-
}
|
103
|
-
|
104
103
|
|
105
104
|
@dataclass
|
106
105
|
class NullByteInHeader(Probe):
|
107
106
|
"""Support NULL bytes in headers."""
|
108
107
|
|
109
|
-
name: str = "
|
108
|
+
name: str = "Supports NULL byte in headers"
|
110
109
|
|
111
110
|
def prepare_request(
|
112
|
-
self, session: requests.Session, request: requests.Request, schema: BaseSchema
|
111
|
+
self, session: requests.Session, request: requests.Request, schema: BaseSchema
|
113
112
|
) -> requests.PreparedRequest:
|
114
113
|
request.method = "GET"
|
115
|
-
request.url =
|
114
|
+
request.url = schema.get_base_url()
|
116
115
|
request.headers = {"X-Schemathesis-Probe-Null": "\x00"}
|
117
116
|
return session.prepare_request(request)
|
118
117
|
|
@@ -125,22 +124,22 @@ class NullByteInHeader(Probe):
|
|
125
124
|
PROBES = (NullByteInHeader,)
|
126
125
|
|
127
126
|
|
128
|
-
def send(probe: Probe,
|
127
|
+
def send(probe: Probe, ctx: EngineContext) -> ProbeRun:
|
129
128
|
"""Send the probe to the application."""
|
130
129
|
from requests import PreparedRequest, Request, RequestException
|
131
130
|
from requests.exceptions import MissingSchema
|
132
131
|
from urllib3.exceptions import InsecureRequestWarning
|
133
132
|
|
134
133
|
try:
|
135
|
-
|
134
|
+
session = ctx.get_session()
|
135
|
+
request = probe.prepare_request(session, Request(), ctx.schema)
|
136
136
|
request.headers[HEADER_NAME] = probe.name
|
137
137
|
request.headers["User-Agent"] = USER_AGENT
|
138
|
-
|
139
|
-
|
140
|
-
kwargs["proxies"] = {"all": config.request.proxy}
|
138
|
+
for header, value in get_default_headers().items():
|
139
|
+
request.headers.setdefault(header, value)
|
141
140
|
with warnings.catch_warnings():
|
142
141
|
warnings.simplefilter("ignore", InsecureRequestWarning)
|
143
|
-
response = session.send(request,
|
142
|
+
response = session.send(request, timeout=ctx.config.request_timeout or 2)
|
144
143
|
except MissingSchema:
|
145
144
|
# In-process ASGI/WSGI testing will have local URLs and requires extra handling
|
146
145
|
# which is not currently implemented
|
@@ -150,18 +149,3 @@ def send(probe: Probe, session: requests.Session, schema: BaseSchema, config: Pr
|
|
150
149
|
return ProbeRun(probe, ProbeOutcome.ERROR, req, None, exc)
|
151
150
|
result_type = probe.analyze_response(response)
|
152
151
|
return ProbeRun(probe, result_type, request, response)
|
153
|
-
|
154
|
-
|
155
|
-
def run(schema: BaseSchema, config: ProbeConfig) -> list[ProbeRun]:
|
156
|
-
"""Run all probes against the given schema."""
|
157
|
-
from requests import Session
|
158
|
-
|
159
|
-
session = Session()
|
160
|
-
session.headers.update(config.headers or {})
|
161
|
-
session.verify = config.request.tls_verify
|
162
|
-
if config.request.cert is not None:
|
163
|
-
session.cert = config.request.cert
|
164
|
-
if config.auth is not None:
|
165
|
-
session.auth = get_requests_auth(config.auth, config.auth_type)
|
166
|
-
|
167
|
-
return [send(probe(), session, schema, config) for probe in PROBES]
|
@@ -0,0 +1,68 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import queue
|
4
|
+
import threading
|
5
|
+
from typing import TYPE_CHECKING
|
6
|
+
|
7
|
+
from schemathesis.engine import Status, events
|
8
|
+
from schemathesis.engine.phases import Phase, PhaseName, PhaseSkipReason
|
9
|
+
from schemathesis.generation.stateful import STATEFUL_TESTS_LABEL
|
10
|
+
|
11
|
+
if TYPE_CHECKING:
|
12
|
+
from schemathesis.engine.context import EngineContext
|
13
|
+
|
14
|
+
EVENT_QUEUE_TIMEOUT = 0.01
|
15
|
+
|
16
|
+
|
17
|
+
def execute(engine: EngineContext, phase: Phase) -> events.EventGenerator:
|
18
|
+
from schemathesis.engine.phases.stateful._executor import execute_state_machine_loop
|
19
|
+
|
20
|
+
try:
|
21
|
+
state_machine = engine.schema.as_state_machine()
|
22
|
+
except Exception as exc:
|
23
|
+
yield events.NonFatalError(error=exc, phase=phase.name, label=STATEFUL_TESTS_LABEL, related_to_operation=False)
|
24
|
+
yield events.PhaseFinished(phase=phase, status=Status.ERROR, payload=None)
|
25
|
+
return
|
26
|
+
|
27
|
+
event_queue: queue.Queue = queue.Queue()
|
28
|
+
|
29
|
+
thread = threading.Thread(
|
30
|
+
target=execute_state_machine_loop,
|
31
|
+
kwargs={"state_machine": state_machine, "event_queue": event_queue, "engine": engine},
|
32
|
+
name="schemathesis_stateful_tests",
|
33
|
+
)
|
34
|
+
status: Status | None = None
|
35
|
+
is_executed = False
|
36
|
+
|
37
|
+
thread.start()
|
38
|
+
try:
|
39
|
+
while True:
|
40
|
+
try:
|
41
|
+
event = event_queue.get(timeout=EVENT_QUEUE_TIMEOUT)
|
42
|
+
is_executed = True
|
43
|
+
# Set the run status based on the suite status
|
44
|
+
# ERROR & INTERRUPTED statuses are terminal, therefore they should not be overridden
|
45
|
+
if (
|
46
|
+
isinstance(event, events.SuiteFinished)
|
47
|
+
and event.status != Status.SKIP
|
48
|
+
and (status is None or status < event.status)
|
49
|
+
):
|
50
|
+
status = event.status
|
51
|
+
yield event
|
52
|
+
except queue.Empty:
|
53
|
+
if not thread.is_alive():
|
54
|
+
break
|
55
|
+
except KeyboardInterrupt:
|
56
|
+
# Immediately notify the engine thread to stop, even though that the event will be set below in `finally`
|
57
|
+
engine.stop()
|
58
|
+
status = Status.INTERRUPTED
|
59
|
+
yield events.Interrupted(phase=PhaseName.STATEFUL_TESTING)
|
60
|
+
finally:
|
61
|
+
thread.join()
|
62
|
+
|
63
|
+
if not is_executed:
|
64
|
+
phase.skip_reason = PhaseSkipReason.NOTHING_TO_TEST
|
65
|
+
status = Status.SKIP
|
66
|
+
elif status is None:
|
67
|
+
status = Status.SKIP
|
68
|
+
yield events.PhaseFinished(phase=phase, status=status, payload=None)
|