schemathesis 3.25.6__py3-none-any.whl → 4.0.0a1__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 +27 -65
- schemathesis/auths.py +102 -82
- schemathesis/checks.py +126 -46
- schemathesis/cli/__init__.py +11 -1760
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +37 -0
- schemathesis/cli/commands/run/__init__.py +662 -0
- schemathesis/cli/commands/run/checks.py +80 -0
- schemathesis/cli/commands/run/context.py +117 -0
- schemathesis/cli/commands/run/events.py +35 -0
- schemathesis/cli/commands/run/executor.py +138 -0
- schemathesis/cli/commands/run/filters.py +194 -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 +494 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
- schemathesis/cli/commands/run/handlers/output.py +746 -0
- schemathesis/cli/commands/run/hypothesis.py +105 -0
- schemathesis/cli/commands/run/loaders.py +129 -0
- schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
- schemathesis/cli/constants.py +5 -52
- schemathesis/cli/core.py +17 -0
- schemathesis/cli/ext/fs.py +14 -0
- schemathesis/cli/ext/groups.py +55 -0
- schemathesis/cli/{options.py → ext/options.py} +39 -10
- schemathesis/cli/hooks.py +36 -0
- schemathesis/contrib/__init__.py +1 -3
- schemathesis/contrib/openapi/__init__.py +1 -3
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
- schemathesis/core/__init__.py +58 -0
- schemathesis/core/compat.py +25 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +370 -0
- schemathesis/core/failures.py +285 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
- schemathesis/core/output/__init__.py +69 -0
- schemathesis/core/output/sanitization.py +197 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +31 -0
- schemathesis/{internal → core}/result.py +1 -1
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +108 -0
- schemathesis/core/validation.py +38 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +30 -0
- schemathesis/engine/config.py +59 -0
- schemathesis/engine/context.py +119 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +157 -0
- schemathesis/engine/errors.py +394 -0
- schemathesis/engine/events.py +337 -0
- schemathesis/engine/phases/__init__.py +66 -0
- schemathesis/{runner → engine/phases}/probes.py +50 -67
- schemathesis/engine/phases/stateful/__init__.py +65 -0
- schemathesis/engine/phases/stateful/_executor.py +326 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +174 -0
- schemathesis/engine/phases/unit/_executor.py +321 -0
- schemathesis/engine/phases/unit/_pool.py +74 -0
- schemathesis/engine/recorder.py +241 -0
- schemathesis/errors.py +31 -0
- schemathesis/experimental/__init__.py +18 -14
- schemathesis/filters.py +103 -14
- schemathesis/generation/__init__.py +21 -37
- schemathesis/generation/case.py +190 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/generation/hypothesis/__init__.py +30 -0
- schemathesis/generation/hypothesis/builder.py +585 -0
- schemathesis/generation/hypothesis/examples.py +50 -0
- 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/modes.py +28 -0
- schemathesis/generation/overrides.py +96 -0
- schemathesis/generation/stateful/__init__.py +20 -0
- schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
- schemathesis/generation/targets.py +69 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +115 -0
- schemathesis/graphql/loaders.py +131 -0
- schemathesis/hooks.py +99 -67
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +412 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +63 -0
- schemathesis/openapi/loaders.py +178 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +273 -0
- schemathesis/pytest/loaders.py +12 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -261
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +25 -0
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +7 -5
- schemathesis/specs/graphql/schemas.py +215 -187
- schemathesis/specs/graphql/validation.py +11 -18
- schemathesis/specs/openapi/__init__.py +7 -1
- schemathesis/specs/openapi/_cache.py +122 -0
- schemathesis/specs/openapi/_hypothesis.py +146 -165
- schemathesis/specs/openapi/checks.py +565 -67
- schemathesis/specs/openapi/converter.py +33 -6
- schemathesis/specs/openapi/definitions.py +11 -18
- schemathesis/specs/openapi/examples.py +139 -23
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +4 -6
- schemathesis/specs/openapi/expressions/extractors.py +23 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +38 -14
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +45 -0
- schemathesis/specs/openapi/links.py +65 -165
- schemathesis/specs/openapi/media_types.py +32 -0
- schemathesis/specs/openapi/negative/__init__.py +7 -3
- schemathesis/specs/openapi/negative/mutations.py +24 -8
- schemathesis/specs/openapi/parameters.py +46 -30
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +47 -57
- schemathesis/specs/openapi/schemas.py +478 -369
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +11 -6
- schemathesis/specs/openapi/stateful/__init__.py +185 -73
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/transport/__init__.py +104 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +99 -0
- schemathesis/transport/requests.py +221 -0
- schemathesis/{_xml.py → transport/serialization.py} +143 -28
- schemathesis/transport/wsgi.py +165 -0
- schemathesis-4.0.0a1.dist-info/METADATA +297 -0
- schemathesis-4.0.0a1.dist-info/RECORD +152 -0
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -17
- schemathesis/_hypothesis.py +0 -246
- schemathesis/_override.py +0 -49
- schemathesis/cli/cassettes.py +0 -375
- schemathesis/cli/context.py +0 -58
- schemathesis/cli/debug.py +0 -26
- schemathesis/cli/handlers.py +0 -16
- schemathesis/cli/junitxml.py +0 -43
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -790
- schemathesis/cli/output/short.py +0 -44
- schemathesis/cli/sanitization.py +0 -20
- schemathesis/code_samples.py +0 -149
- schemathesis/constants.py +0 -55
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -15
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -560
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -17
- schemathesis/failures.py +0 -209
- schemathesis/fixups/__init__.py +0 -36
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -29
- schemathesis/graphql.py +0 -4
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/copy.py +0 -13
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -34
- schemathesis/internal/jsonschema.py +0 -35
- schemathesis/internal/transformation.py +0 -15
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -361
- schemathesis/loaders.py +0 -120
- schemathesis/models.py +0 -1234
- schemathesis/parameters.py +0 -86
- schemathesis/runner/__init__.py +0 -570
- schemathesis/runner/events.py +0 -329
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -1035
- schemathesis/runner/impl/solo.py +0 -90
- schemathesis/runner/impl/threadpool.py +0 -400
- schemathesis/runner/serialization.py +0 -411
- schemathesis/sanitization.py +0 -248
- schemathesis/serializers.py +0 -323
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -201
- schemathesis/service/client.py +0 -100
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -57
- schemathesis/service/hosts.py +0 -107
- schemathesis/service/metadata.py +0 -46
- schemathesis/service/models.py +0 -49
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -199
- schemathesis/service/usage.py +0 -65
- schemathesis/specs/graphql/loaders.py +0 -344
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/loaders.py +0 -667
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis/specs/openapi/validation.py +0 -25
- schemathesis/stateful/__init__.py +0 -133
- schemathesis/targets.py +0 -45
- schemathesis/throttling.py +0 -41
- schemathesis/transports/__init__.py +0 -5
- schemathesis/transports/auth.py +0 -15
- schemathesis/transports/headers.py +0 -35
- schemathesis/transports/responses.py +0 -52
- schemathesis/types.py +0 -35
- schemathesis/utils.py +0 -169
- schemathesis-3.25.6.dist-info/METADATA +0 -356
- schemathesis-3.25.6.dist-info/RECORD +0 -134
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,326 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import queue
|
4
|
+
import time
|
5
|
+
import unittest
|
6
|
+
from dataclasses import replace
|
7
|
+
from typing import Any
|
8
|
+
|
9
|
+
import hypothesis
|
10
|
+
from hypothesis.control import current_build_context
|
11
|
+
from hypothesis.errors import Flaky, Unsatisfiable
|
12
|
+
from hypothesis.stateful import Rule
|
13
|
+
|
14
|
+
from schemathesis.checks import CheckContext, CheckFunction, run_checks
|
15
|
+
from schemathesis.core.failures import Failure, FailureGroup
|
16
|
+
from schemathesis.core.transport import Response
|
17
|
+
from schemathesis.engine import Status, events
|
18
|
+
from schemathesis.engine.context import EngineContext
|
19
|
+
from schemathesis.engine.control import ExecutionControl
|
20
|
+
from schemathesis.engine.phases import PhaseName
|
21
|
+
from schemathesis.engine.phases.stateful.context import StatefulContext
|
22
|
+
from schemathesis.engine.recorder import ScenarioRecorder
|
23
|
+
from schemathesis.generation.case import Case
|
24
|
+
from schemathesis.generation.hypothesis.reporting import ignore_hypothesis_output
|
25
|
+
from schemathesis.generation.stateful.state_machine import (
|
26
|
+
DEFAULT_STATE_MACHINE_SETTINGS,
|
27
|
+
APIStateMachine,
|
28
|
+
Direction,
|
29
|
+
StepResult,
|
30
|
+
)
|
31
|
+
from schemathesis.generation.targets import TargetMetricCollector
|
32
|
+
|
33
|
+
|
34
|
+
def _get_hypothesis_settings_kwargs_override(settings: hypothesis.settings) -> dict[str, Any]:
|
35
|
+
"""Get the settings that should be overridden to match the defaults for API state machines."""
|
36
|
+
kwargs = {}
|
37
|
+
hypothesis_default = hypothesis.settings()
|
38
|
+
if settings.phases == hypothesis_default.phases:
|
39
|
+
kwargs["phases"] = DEFAULT_STATE_MACHINE_SETTINGS.phases
|
40
|
+
if settings.stateful_step_count == hypothesis_default.stateful_step_count:
|
41
|
+
kwargs["stateful_step_count"] = DEFAULT_STATE_MACHINE_SETTINGS.stateful_step_count
|
42
|
+
if settings.deadline == hypothesis_default.deadline:
|
43
|
+
kwargs["deadline"] = DEFAULT_STATE_MACHINE_SETTINGS.deadline
|
44
|
+
if settings.suppress_health_check == hypothesis_default.suppress_health_check:
|
45
|
+
kwargs["suppress_health_check"] = DEFAULT_STATE_MACHINE_SETTINGS.suppress_health_check
|
46
|
+
return kwargs
|
47
|
+
|
48
|
+
|
49
|
+
def execute_state_machine_loop(
|
50
|
+
*,
|
51
|
+
state_machine: type[APIStateMachine],
|
52
|
+
event_queue: queue.Queue,
|
53
|
+
engine: EngineContext,
|
54
|
+
) -> None:
|
55
|
+
"""Execute the state machine testing loop."""
|
56
|
+
kwargs = _get_hypothesis_settings_kwargs_override(engine.config.execution.hypothesis_settings)
|
57
|
+
if kwargs:
|
58
|
+
config = replace(
|
59
|
+
engine.config,
|
60
|
+
execution=replace(
|
61
|
+
engine.config.execution,
|
62
|
+
hypothesis_settings=hypothesis.settings(engine.config.execution.hypothesis_settings, **kwargs),
|
63
|
+
),
|
64
|
+
)
|
65
|
+
else:
|
66
|
+
config = engine.config
|
67
|
+
|
68
|
+
ctx = StatefulContext(metric_collector=TargetMetricCollector(targets=config.execution.targets))
|
69
|
+
|
70
|
+
transport_kwargs = engine.transport_kwargs
|
71
|
+
|
72
|
+
class _InstrumentedStateMachine(state_machine): # type: ignore[valid-type,misc]
|
73
|
+
"""State machine with additional hooks for emitting events."""
|
74
|
+
|
75
|
+
def setup(self) -> None:
|
76
|
+
scenario_started = events.ScenarioStarted(label=None, phase=PhaseName.STATEFUL_TESTING, suite_id=suite_id)
|
77
|
+
self._start_time = time.monotonic()
|
78
|
+
self._scenario_id = scenario_started.id
|
79
|
+
event_queue.put(scenario_started)
|
80
|
+
self.recorder = ScenarioRecorder(label="Stateful tests")
|
81
|
+
self._check_ctx = engine.get_check_context(self.recorder)
|
82
|
+
|
83
|
+
def get_call_kwargs(self, case: Case) -> dict[str, Any]:
|
84
|
+
return transport_kwargs
|
85
|
+
|
86
|
+
def _repr_step(self, rule: Rule, data: dict, result: StepResult) -> str:
|
87
|
+
return ""
|
88
|
+
|
89
|
+
if config.override is not None:
|
90
|
+
|
91
|
+
def before_call(self, case: Case) -> None:
|
92
|
+
for location, entry in config.override.for_operation(case.operation).items(): # type: ignore[union-attr]
|
93
|
+
if entry:
|
94
|
+
container = getattr(case, location) or {}
|
95
|
+
container.update(entry)
|
96
|
+
setattr(case, location, container)
|
97
|
+
return super().before_call(case)
|
98
|
+
|
99
|
+
def step(self, case: Case, previous: tuple[StepResult, Direction] | None = None) -> StepResult | None:
|
100
|
+
# Checking the stop event once inside `step` is sufficient as it is called frequently
|
101
|
+
# The idea is to stop the execution as soon as possible
|
102
|
+
if previous is not None:
|
103
|
+
step_result, _ = previous
|
104
|
+
self.recorder.record_case(parent_id=step_result.case.id, case=case)
|
105
|
+
else:
|
106
|
+
self.recorder.record_case(parent_id=None, case=case)
|
107
|
+
if engine.has_to_stop:
|
108
|
+
raise KeyboardInterrupt
|
109
|
+
step_started = events.StepStarted(
|
110
|
+
phase=PhaseName.STATEFUL_TESTING, suite_id=suite_id, scenario_id=self._scenario_id
|
111
|
+
)
|
112
|
+
event_queue.put(step_started)
|
113
|
+
try:
|
114
|
+
if config.execution.unique_inputs:
|
115
|
+
cached = ctx.get_step_outcome(case)
|
116
|
+
if isinstance(cached, BaseException):
|
117
|
+
raise cached
|
118
|
+
elif cached is None:
|
119
|
+
return None
|
120
|
+
result = super().step(case, previous)
|
121
|
+
ctx.step_succeeded()
|
122
|
+
except FailureGroup as exc:
|
123
|
+
if config.execution.unique_inputs:
|
124
|
+
for failure in exc.exceptions:
|
125
|
+
ctx.store_step_outcome(case, failure)
|
126
|
+
ctx.step_failed()
|
127
|
+
raise
|
128
|
+
except Exception as exc:
|
129
|
+
if config.execution.unique_inputs:
|
130
|
+
ctx.store_step_outcome(case, exc)
|
131
|
+
ctx.step_errored()
|
132
|
+
raise
|
133
|
+
except KeyboardInterrupt:
|
134
|
+
ctx.step_interrupted()
|
135
|
+
raise
|
136
|
+
except BaseException as exc:
|
137
|
+
if config.execution.unique_inputs:
|
138
|
+
ctx.store_step_outcome(case, exc)
|
139
|
+
raise exc
|
140
|
+
else:
|
141
|
+
if config.execution.unique_inputs:
|
142
|
+
ctx.store_step_outcome(case, None)
|
143
|
+
finally:
|
144
|
+
transition_id: events.TransitionId | None
|
145
|
+
if previous is not None:
|
146
|
+
transition = previous[1]
|
147
|
+
transition_id = events.TransitionId(
|
148
|
+
name=transition.name,
|
149
|
+
status_code=transition.status_code,
|
150
|
+
source=transition.operation.label,
|
151
|
+
)
|
152
|
+
else:
|
153
|
+
transition_id = None
|
154
|
+
event_queue.put(
|
155
|
+
events.StepFinished(
|
156
|
+
id=step_started.id,
|
157
|
+
suite_id=suite_id,
|
158
|
+
scenario_id=self._scenario_id,
|
159
|
+
phase=PhaseName.STATEFUL_TESTING,
|
160
|
+
status=ctx.current_step_status,
|
161
|
+
transition_id=transition_id,
|
162
|
+
target=case.operation.label,
|
163
|
+
response=ctx.current_response,
|
164
|
+
)
|
165
|
+
)
|
166
|
+
return result
|
167
|
+
|
168
|
+
def validate_response(
|
169
|
+
self, response: Response, case: Case, additional_checks: tuple[CheckFunction, ...] = ()
|
170
|
+
) -> None:
|
171
|
+
self.recorder.record_response(case_id=case.id, response=response)
|
172
|
+
ctx.collect_metric(case, response)
|
173
|
+
ctx.current_response = response
|
174
|
+
validate_response(
|
175
|
+
response=response,
|
176
|
+
case=case,
|
177
|
+
stateful_ctx=ctx,
|
178
|
+
check_ctx=self._check_ctx,
|
179
|
+
checks=config.execution.checks,
|
180
|
+
control=engine.control,
|
181
|
+
recorder=self.recorder,
|
182
|
+
additional_checks=additional_checks,
|
183
|
+
)
|
184
|
+
|
185
|
+
def teardown(self) -> None:
|
186
|
+
build_ctx = current_build_context()
|
187
|
+
event_queue.put(
|
188
|
+
events.ScenarioFinished(
|
189
|
+
id=self._scenario_id,
|
190
|
+
suite_id=suite_id,
|
191
|
+
phase=PhaseName.STATEFUL_TESTING,
|
192
|
+
# With dry run there will be no status
|
193
|
+
status=ctx.current_scenario_status or Status.SKIP,
|
194
|
+
recorder=self.recorder,
|
195
|
+
elapsed_time=time.monotonic() - self._start_time,
|
196
|
+
skip_reason=None,
|
197
|
+
is_final=build_ctx.is_final,
|
198
|
+
)
|
199
|
+
)
|
200
|
+
ctx.maximize_metrics()
|
201
|
+
ctx.reset_scenario()
|
202
|
+
super().teardown()
|
203
|
+
|
204
|
+
if config.execution.seed is not None:
|
205
|
+
InstrumentedStateMachine = hypothesis.seed(config.execution.seed)(_InstrumentedStateMachine)
|
206
|
+
else:
|
207
|
+
InstrumentedStateMachine = _InstrumentedStateMachine
|
208
|
+
|
209
|
+
while True:
|
210
|
+
# This loop is running until no new failures are found in a single iteration
|
211
|
+
suite_started = events.SuiteStarted(phase=PhaseName.STATEFUL_TESTING)
|
212
|
+
suite_id = suite_started.id
|
213
|
+
event_queue.put(suite_started)
|
214
|
+
if engine.is_interrupted:
|
215
|
+
event_queue.put(events.Interrupted(phase=PhaseName.STATEFUL_TESTING))
|
216
|
+
event_queue.put(
|
217
|
+
events.SuiteFinished(
|
218
|
+
id=suite_started.id,
|
219
|
+
phase=PhaseName.STATEFUL_TESTING,
|
220
|
+
status=Status.INTERRUPTED,
|
221
|
+
)
|
222
|
+
)
|
223
|
+
break
|
224
|
+
suite_status = Status.SUCCESS
|
225
|
+
try:
|
226
|
+
with ignore_hypothesis_output(): # type: ignore
|
227
|
+
InstrumentedStateMachine.run(settings=config.execution.hypothesis_settings)
|
228
|
+
except KeyboardInterrupt:
|
229
|
+
# Raised in the state machine when the stop event is set or it is raised by the user's code
|
230
|
+
# that is placed in the base class of the state machine.
|
231
|
+
# Therefore, set the stop event to cover the latter case
|
232
|
+
engine.stop()
|
233
|
+
suite_status = Status.INTERRUPTED
|
234
|
+
event_queue.put(events.Interrupted(phase=PhaseName.STATEFUL_TESTING))
|
235
|
+
break
|
236
|
+
except unittest.case.SkipTest:
|
237
|
+
# If `explicit` phase is used and there are not examples
|
238
|
+
suite_status = Status.SKIP
|
239
|
+
break
|
240
|
+
except FailureGroup as exc:
|
241
|
+
# When a check fails, the state machine is stopped
|
242
|
+
# The failure is already sent to the queue by the state machine
|
243
|
+
# Here we need to either exit or re-run the state machine with this failure marked as known
|
244
|
+
suite_status = Status.FAILURE
|
245
|
+
if engine.has_reached_the_failure_limit:
|
246
|
+
break # type: ignore[unreachable]
|
247
|
+
for failure in exc.exceptions:
|
248
|
+
ctx.mark_as_seen_in_run(failure)
|
249
|
+
continue
|
250
|
+
except Flaky:
|
251
|
+
suite_status = Status.FAILURE
|
252
|
+
if engine.has_reached_the_failure_limit:
|
253
|
+
break # type: ignore[unreachable]
|
254
|
+
# Mark all failures in this suite as seen to prevent them being re-discovered
|
255
|
+
ctx.mark_current_suite_as_seen_in_run()
|
256
|
+
continue
|
257
|
+
except Exception as exc:
|
258
|
+
if isinstance(exc, Unsatisfiable) and ctx.completed_scenarios > 0:
|
259
|
+
# Sometimes Hypothesis randomly gives up on generating some complex cases. However, if we know that
|
260
|
+
# values are possible to generate based on the previous observations, we retry the generation
|
261
|
+
if ctx.completed_scenarios >= config.execution.hypothesis_settings.max_examples:
|
262
|
+
# Avoid infinite restarts
|
263
|
+
break
|
264
|
+
continue
|
265
|
+
# Any other exception is an inner error and the test run should be stopped
|
266
|
+
suite_status = Status.ERROR
|
267
|
+
event_queue.put(
|
268
|
+
events.NonFatalError(
|
269
|
+
error=exc, phase=PhaseName.STATEFUL_TESTING, label="Stateful tests", related_to_operation=False
|
270
|
+
)
|
271
|
+
)
|
272
|
+
break
|
273
|
+
finally:
|
274
|
+
event_queue.put(
|
275
|
+
events.SuiteFinished(
|
276
|
+
id=suite_started.id,
|
277
|
+
phase=PhaseName.STATEFUL_TESTING,
|
278
|
+
status=suite_status,
|
279
|
+
)
|
280
|
+
)
|
281
|
+
ctx.reset()
|
282
|
+
# Exit on the first successful state machine execution
|
283
|
+
break
|
284
|
+
|
285
|
+
|
286
|
+
def validate_response(
|
287
|
+
*,
|
288
|
+
response: Response,
|
289
|
+
case: Case,
|
290
|
+
stateful_ctx: StatefulContext,
|
291
|
+
check_ctx: CheckContext,
|
292
|
+
control: ExecutionControl,
|
293
|
+
checks: list[CheckFunction],
|
294
|
+
recorder: ScenarioRecorder,
|
295
|
+
additional_checks: tuple[CheckFunction, ...] = (),
|
296
|
+
) -> None:
|
297
|
+
"""Validate the response against the provided checks."""
|
298
|
+
|
299
|
+
def on_failure(name: str, collected: set[Failure], failure: Failure) -> None:
|
300
|
+
if stateful_ctx.is_seen_in_suite(failure) or stateful_ctx.is_seen_in_run(failure):
|
301
|
+
return
|
302
|
+
failure_data = recorder.find_failure_data(parent_id=case.id, failure=failure)
|
303
|
+
recorder.record_check_failure(
|
304
|
+
name=name,
|
305
|
+
case_id=failure_data.case.id,
|
306
|
+
code_sample=failure_data.case.as_curl_command(headers=failure_data.headers, verify=failure_data.verify),
|
307
|
+
failure=failure,
|
308
|
+
)
|
309
|
+
control.count_failure()
|
310
|
+
stateful_ctx.mark_as_seen_in_suite(failure)
|
311
|
+
collected.add(failure)
|
312
|
+
|
313
|
+
def on_success(name: str, case: Case) -> None:
|
314
|
+
recorder.record_check_success(name=name, case_id=case.id)
|
315
|
+
|
316
|
+
failures = run_checks(
|
317
|
+
case=case,
|
318
|
+
response=response,
|
319
|
+
ctx=check_ctx,
|
320
|
+
checks=tuple(checks) + tuple(additional_checks),
|
321
|
+
on_failure=on_failure,
|
322
|
+
on_success=on_success,
|
323
|
+
)
|
324
|
+
|
325
|
+
if failures:
|
326
|
+
raise FailureGroup(list(failures)) from None
|
@@ -0,0 +1,85 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from dataclasses import dataclass, field
|
4
|
+
|
5
|
+
from schemathesis.core import NOT_SET, NotSet
|
6
|
+
from schemathesis.core.failures import Failure
|
7
|
+
from schemathesis.core.transport import Response
|
8
|
+
from schemathesis.engine import Status
|
9
|
+
from schemathesis.generation.case import Case
|
10
|
+
from schemathesis.generation.targets import TargetMetricCollector
|
11
|
+
|
12
|
+
|
13
|
+
@dataclass
|
14
|
+
class StatefulContext:
|
15
|
+
"""Mutable context for state machine execution."""
|
16
|
+
|
17
|
+
# All seen failure keys, both grouped and individual ones
|
18
|
+
seen_in_run: set[Failure] = field(default_factory=set)
|
19
|
+
# Failures keys seen in the current suite
|
20
|
+
seen_in_suite: set[Failure] = field(default_factory=set)
|
21
|
+
# Status of the current step
|
22
|
+
current_step_status: Status | None = None
|
23
|
+
# The currently processed response
|
24
|
+
current_response: Response | None = None
|
25
|
+
# Total number of failures
|
26
|
+
failures_count: int = 0
|
27
|
+
# The total number of completed test scenario
|
28
|
+
completed_scenarios: int = 0
|
29
|
+
# Metrics collector for targeted testing
|
30
|
+
metric_collector: TargetMetricCollector = field(default_factory=TargetMetricCollector)
|
31
|
+
step_outcomes: dict[int, BaseException | None] = field(default_factory=dict)
|
32
|
+
|
33
|
+
@property
|
34
|
+
def current_scenario_status(self) -> Status | None:
|
35
|
+
return self.current_step_status
|
36
|
+
|
37
|
+
def reset_scenario(self) -> None:
|
38
|
+
self.completed_scenarios += 1
|
39
|
+
self.current_step_status = None
|
40
|
+
self.current_response = None
|
41
|
+
self.step_outcomes.clear()
|
42
|
+
|
43
|
+
def step_succeeded(self) -> None:
|
44
|
+
self.current_step_status = Status.SUCCESS
|
45
|
+
|
46
|
+
def step_failed(self) -> None:
|
47
|
+
self.current_step_status = Status.FAILURE
|
48
|
+
|
49
|
+
def step_errored(self) -> None:
|
50
|
+
self.current_step_status = Status.ERROR
|
51
|
+
|
52
|
+
def step_interrupted(self) -> None:
|
53
|
+
self.current_step_status = Status.INTERRUPTED
|
54
|
+
|
55
|
+
def mark_as_seen_in_run(self, exc: Failure) -> None:
|
56
|
+
self.seen_in_run.add(exc)
|
57
|
+
|
58
|
+
def mark_as_seen_in_suite(self, exc: Failure) -> None:
|
59
|
+
self.seen_in_suite.add(exc)
|
60
|
+
|
61
|
+
def mark_current_suite_as_seen_in_run(self) -> None:
|
62
|
+
self.seen_in_run.update(self.seen_in_suite)
|
63
|
+
|
64
|
+
def is_seen_in_run(self, exc: Failure) -> bool:
|
65
|
+
return exc in self.seen_in_run
|
66
|
+
|
67
|
+
def is_seen_in_suite(self, exc: Failure) -> bool:
|
68
|
+
return exc in self.seen_in_suite
|
69
|
+
|
70
|
+
def collect_metric(self, case: Case, response: Response) -> None:
|
71
|
+
self.metric_collector.store(case, response)
|
72
|
+
|
73
|
+
def maximize_metrics(self) -> None:
|
74
|
+
self.metric_collector.maximize()
|
75
|
+
|
76
|
+
def reset(self) -> None:
|
77
|
+
self.seen_in_suite.clear()
|
78
|
+
self.reset_scenario()
|
79
|
+
self.metric_collector.reset()
|
80
|
+
|
81
|
+
def store_step_outcome(self, case: Case, outcome: BaseException | None) -> None:
|
82
|
+
self.step_outcomes[hash(case)] = outcome
|
83
|
+
|
84
|
+
def get_step_outcome(self, case: Case) -> BaseException | None | NotSet:
|
85
|
+
return self.step_outcomes.get(hash(case), NOT_SET)
|
@@ -0,0 +1,174 @@
|
|
1
|
+
"""Unit testing by Schemathesis Engine.
|
2
|
+
|
3
|
+
This module provides high-level flow for single-, and multi-threaded modes.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from __future__ import annotations
|
7
|
+
|
8
|
+
import queue
|
9
|
+
import uuid
|
10
|
+
import warnings
|
11
|
+
from queue import Queue
|
12
|
+
from typing import TYPE_CHECKING, Any
|
13
|
+
|
14
|
+
from schemathesis.core.result import Ok
|
15
|
+
from schemathesis.engine import Status, events
|
16
|
+
from schemathesis.engine.phases import PhaseName, PhaseSkipReason
|
17
|
+
from schemathesis.engine.recorder import ScenarioRecorder
|
18
|
+
from schemathesis.generation.hypothesis.builder import HypothesisTestConfig
|
19
|
+
from schemathesis.generation.hypothesis.reporting import ignore_hypothesis_output
|
20
|
+
|
21
|
+
from ._pool import TaskProducer, WorkerPool
|
22
|
+
|
23
|
+
if TYPE_CHECKING:
|
24
|
+
from schemathesis.engine.context import EngineContext
|
25
|
+
from schemathesis.engine.phases import Phase
|
26
|
+
from schemathesis.schemas import APIOperation
|
27
|
+
|
28
|
+
WORKER_TIMEOUT = 0.1
|
29
|
+
|
30
|
+
|
31
|
+
def execute(engine: EngineContext, phase: Phase) -> events.EventGenerator:
|
32
|
+
"""Run a set of unit tests.
|
33
|
+
|
34
|
+
Implemented as a producer-consumer pattern via a task queue.
|
35
|
+
The main thread provides an iterator over API operations and worker threads create test functions and run them.
|
36
|
+
"""
|
37
|
+
producer = TaskProducer(engine)
|
38
|
+
workers_num = engine.config.execution.workers_num
|
39
|
+
|
40
|
+
suite_started = events.SuiteStarted(phase=phase.name)
|
41
|
+
|
42
|
+
yield suite_started
|
43
|
+
|
44
|
+
status = None
|
45
|
+
is_executed = False
|
46
|
+
|
47
|
+
with WorkerPool(
|
48
|
+
workers_num=workers_num, producer=producer, worker_factory=worker_task, ctx=engine, suite_id=suite_started.id
|
49
|
+
) as pool:
|
50
|
+
try:
|
51
|
+
while True:
|
52
|
+
try:
|
53
|
+
event = pool.events_queue.get(timeout=WORKER_TIMEOUT)
|
54
|
+
is_executed = True
|
55
|
+
if engine.is_interrupted:
|
56
|
+
raise KeyboardInterrupt
|
57
|
+
yield event
|
58
|
+
if isinstance(event, events.NonFatalError):
|
59
|
+
status = Status.ERROR
|
60
|
+
if isinstance(event, events.ScenarioFinished):
|
61
|
+
if event.status != Status.SKIP and (status is None or status < event.status):
|
62
|
+
status = event.status
|
63
|
+
if event.status in (Status.ERROR, Status.FAILURE):
|
64
|
+
engine.control.count_failure()
|
65
|
+
if isinstance(event, events.Interrupted) or engine.is_interrupted:
|
66
|
+
status = Status.INTERRUPTED
|
67
|
+
engine.stop()
|
68
|
+
if engine.has_to_stop:
|
69
|
+
break # type: ignore[unreachable]
|
70
|
+
except queue.Empty:
|
71
|
+
if all(not worker.is_alive() for worker in pool.workers):
|
72
|
+
break
|
73
|
+
continue
|
74
|
+
except KeyboardInterrupt:
|
75
|
+
engine.stop()
|
76
|
+
status = Status.INTERRUPTED
|
77
|
+
yield events.Interrupted(phase=PhaseName.UNIT_TESTING)
|
78
|
+
|
79
|
+
if not is_executed:
|
80
|
+
phase.skip_reason = PhaseSkipReason.NOTHING_TO_TEST
|
81
|
+
status = Status.SKIP
|
82
|
+
elif status is None:
|
83
|
+
status = Status.SKIP
|
84
|
+
# NOTE: Right now there is just one suite, hence two events go one after another
|
85
|
+
yield events.SuiteFinished(id=suite_started.id, phase=phase.name, status=status)
|
86
|
+
yield events.PhaseFinished(phase=phase, status=status, payload=None)
|
87
|
+
|
88
|
+
|
89
|
+
def worker_task(*, events_queue: Queue, producer: TaskProducer, ctx: EngineContext, suite_id: uuid.UUID) -> None:
|
90
|
+
from hypothesis.errors import HypothesisWarning
|
91
|
+
|
92
|
+
from schemathesis.generation.hypothesis.builder import create_test
|
93
|
+
|
94
|
+
from ._executor import run_test, test_func
|
95
|
+
|
96
|
+
warnings.filterwarnings("ignore", message="The recursion limit will not be reset", category=HypothesisWarning)
|
97
|
+
with ignore_hypothesis_output():
|
98
|
+
try:
|
99
|
+
while not ctx.has_to_stop:
|
100
|
+
result = producer.next_operation()
|
101
|
+
if result is None:
|
102
|
+
break
|
103
|
+
|
104
|
+
if isinstance(result, Ok):
|
105
|
+
operation = result.ok()
|
106
|
+
as_strategy_kwargs = get_strategy_kwargs(ctx, operation)
|
107
|
+
test_function = create_test(
|
108
|
+
operation=operation,
|
109
|
+
test_func=test_func,
|
110
|
+
config=HypothesisTestConfig(
|
111
|
+
settings=ctx.config.execution.hypothesis_settings,
|
112
|
+
seed=ctx.config.execution.seed,
|
113
|
+
generation=ctx.config.execution.generation,
|
114
|
+
as_strategy_kwargs=as_strategy_kwargs,
|
115
|
+
),
|
116
|
+
)
|
117
|
+
|
118
|
+
# The test is blocking, meaning that even if CTRL-C comes to the main thread, this tasks will continue
|
119
|
+
# executing. However, as we set a stop event, it will be checked before the next network request.
|
120
|
+
# However, this is still suboptimal, as there could be slow requests and they will block for longer
|
121
|
+
for event in run_test(operation=operation, test_function=test_function, ctx=ctx, suite_id=suite_id):
|
122
|
+
events_queue.put(event)
|
123
|
+
else:
|
124
|
+
error = result.err()
|
125
|
+
if error.method:
|
126
|
+
label = f"{error.method.upper()} {error.full_path}"
|
127
|
+
scenario_started = events.ScenarioStarted(
|
128
|
+
label=label, phase=PhaseName.UNIT_TESTING, suite_id=suite_id
|
129
|
+
)
|
130
|
+
events_queue.put(scenario_started)
|
131
|
+
|
132
|
+
events_queue.put(
|
133
|
+
events.NonFatalError(
|
134
|
+
error=error, phase=PhaseName.UNIT_TESTING, label=label, related_to_operation=True
|
135
|
+
)
|
136
|
+
)
|
137
|
+
|
138
|
+
events_queue.put(
|
139
|
+
events.ScenarioFinished(
|
140
|
+
id=scenario_started.id,
|
141
|
+
suite_id=suite_id,
|
142
|
+
phase=PhaseName.UNIT_TESTING,
|
143
|
+
status=Status.ERROR,
|
144
|
+
recorder=ScenarioRecorder(label="Error"),
|
145
|
+
elapsed_time=0.0,
|
146
|
+
skip_reason=None,
|
147
|
+
is_final=True,
|
148
|
+
)
|
149
|
+
)
|
150
|
+
else:
|
151
|
+
assert error.full_path is not None
|
152
|
+
events_queue.put(
|
153
|
+
events.NonFatalError(
|
154
|
+
error=error,
|
155
|
+
phase=PhaseName.UNIT_TESTING,
|
156
|
+
label=error.full_path,
|
157
|
+
related_to_operation=False,
|
158
|
+
)
|
159
|
+
)
|
160
|
+
except KeyboardInterrupt:
|
161
|
+
events_queue.put(events.Interrupted(phase=PhaseName.UNIT_TESTING))
|
162
|
+
|
163
|
+
|
164
|
+
def get_strategy_kwargs(ctx: EngineContext, operation: APIOperation) -> dict[str, Any]:
|
165
|
+
kwargs = {}
|
166
|
+
if ctx.config.override is not None:
|
167
|
+
for location, entry in ctx.config.override.for_operation(operation).items():
|
168
|
+
if entry:
|
169
|
+
kwargs[location] = entry
|
170
|
+
if ctx.config.network.headers:
|
171
|
+
kwargs["headers"] = {
|
172
|
+
key: value for key, value in ctx.config.network.headers.items() if key.lower() != "user-agent"
|
173
|
+
}
|
174
|
+
return kwargs
|