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
@@ -1,328 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import re
|
4
|
-
import time
|
5
|
-
from dataclasses import dataclass
|
6
|
-
from functools import lru_cache
|
7
|
-
from typing import TYPE_CHECKING, Any, ClassVar
|
8
|
-
|
9
|
-
from hypothesis.errors import InvalidDefinition
|
10
|
-
from hypothesis.stateful import RuleBasedStateMachine
|
11
|
-
|
12
|
-
from .._dependency_versions import HYPOTHESIS_HAS_STATEFUL_NAMING_IMPROVEMENTS
|
13
|
-
from ..constants import NO_LINKS_ERROR_MESSAGE, NOT_SET
|
14
|
-
from ..exceptions import UsageError
|
15
|
-
from ..internal.checks import CheckFunction
|
16
|
-
from ..models import APIOperation, Case
|
17
|
-
from .config import _default_hypothesis_settings_factory
|
18
|
-
from .runner import StatefulTestRunner, StatefulTestRunnerConfig
|
19
|
-
from .sink import StateMachineSink
|
20
|
-
|
21
|
-
if TYPE_CHECKING:
|
22
|
-
import hypothesis
|
23
|
-
from requests.structures import CaseInsensitiveDict
|
24
|
-
|
25
|
-
from ..schemas import BaseSchema
|
26
|
-
from ..transports.responses import GenericResponse
|
27
|
-
from .statistic import TransitionStats
|
28
|
-
|
29
|
-
|
30
|
-
@dataclass
|
31
|
-
class StepResult:
|
32
|
-
"""Output from a single transition of a state machine."""
|
33
|
-
|
34
|
-
response: GenericResponse
|
35
|
-
case: Case
|
36
|
-
elapsed: float
|
37
|
-
|
38
|
-
|
39
|
-
def _normalize_name(name: str) -> str:
|
40
|
-
return re.sub(r"\W|^(?=\d)", "_", name).replace("__", "_")
|
41
|
-
|
42
|
-
|
43
|
-
class APIStateMachine(RuleBasedStateMachine):
|
44
|
-
"""The base class for state machines generated from API schemas.
|
45
|
-
|
46
|
-
Exposes additional extension points in the testing process.
|
47
|
-
"""
|
48
|
-
|
49
|
-
# This is a convenience attribute, which happened to clash with `RuleBasedStateMachine` instance level attribute
|
50
|
-
# They don't interfere, since it is properly overridden on the Hypothesis side, but it is likely that this
|
51
|
-
# attribute will be renamed in the future
|
52
|
-
bundles: ClassVar[dict[str, CaseInsensitiveDict]] # type: ignore
|
53
|
-
schema: BaseSchema
|
54
|
-
# A template for transition statistics that can be filled with data from the state machine during its execution
|
55
|
-
_transition_stats_template: ClassVar[TransitionStats]
|
56
|
-
|
57
|
-
def __init__(self) -> None:
|
58
|
-
try:
|
59
|
-
super().__init__() # type: ignore
|
60
|
-
except InvalidDefinition as exc:
|
61
|
-
if "defines no rules" in str(exc):
|
62
|
-
raise UsageError(NO_LINKS_ERROR_MESSAGE) from None
|
63
|
-
raise
|
64
|
-
self.setup()
|
65
|
-
|
66
|
-
@classmethod
|
67
|
-
@lru_cache
|
68
|
-
def _to_test_case(cls) -> type:
|
69
|
-
from . import run_state_machine_as_test
|
70
|
-
|
71
|
-
class StateMachineTestCase(RuleBasedStateMachine.TestCase):
|
72
|
-
settings = _default_hypothesis_settings_factory()
|
73
|
-
|
74
|
-
def runTest(self) -> None:
|
75
|
-
run_state_machine_as_test(cls, settings=self.settings)
|
76
|
-
|
77
|
-
runTest.is_hypothesis_test = True # type: ignore[attr-defined]
|
78
|
-
|
79
|
-
StateMachineTestCase.__name__ = cls.__name__ + ".TestCase"
|
80
|
-
StateMachineTestCase.__qualname__ = cls.__qualname__ + ".TestCase"
|
81
|
-
return StateMachineTestCase
|
82
|
-
|
83
|
-
def _pretty_print(self, value: Any) -> str:
|
84
|
-
if isinstance(value, Case):
|
85
|
-
# State machines suppose to be reproducible, hence it is OK to get kwargs here
|
86
|
-
kwargs = self.get_call_kwargs(value)
|
87
|
-
return _print_case(value, kwargs)
|
88
|
-
return super()._pretty_print(value) # type: ignore
|
89
|
-
|
90
|
-
if HYPOTHESIS_HAS_STATEFUL_NAMING_IMPROVEMENTS:
|
91
|
-
|
92
|
-
def _new_name(self, target: str) -> str:
|
93
|
-
target = _normalize_name(target)
|
94
|
-
return super()._new_name(target) # type: ignore
|
95
|
-
|
96
|
-
def _get_target_for_result(self, result: StepResult) -> str | None:
|
97
|
-
raise NotImplementedError
|
98
|
-
|
99
|
-
def _add_result_to_targets(self, targets: tuple[str, ...], result: StepResult | None) -> None:
|
100
|
-
if result is None:
|
101
|
-
return
|
102
|
-
target = self._get_target_for_result(result)
|
103
|
-
if target is not None:
|
104
|
-
super()._add_result_to_targets((target,), result)
|
105
|
-
|
106
|
-
@classmethod
|
107
|
-
def format_rules(cls) -> str:
|
108
|
-
raise NotImplementedError
|
109
|
-
|
110
|
-
@classmethod
|
111
|
-
def run(cls, *, settings: hypothesis.settings | None = None) -> None:
|
112
|
-
"""Run state machine as a test."""
|
113
|
-
from . import run_state_machine_as_test
|
114
|
-
|
115
|
-
return run_state_machine_as_test(cls, settings=settings)
|
116
|
-
|
117
|
-
@classmethod
|
118
|
-
def runner(cls, *, config: StatefulTestRunnerConfig | None = None) -> StatefulTestRunner:
|
119
|
-
"""Create a runner for this state machine."""
|
120
|
-
from .runner import StatefulTestRunnerConfig
|
121
|
-
|
122
|
-
return StatefulTestRunner(cls, config=config or StatefulTestRunnerConfig())
|
123
|
-
|
124
|
-
@classmethod
|
125
|
-
def sink(cls) -> StateMachineSink:
|
126
|
-
"""Create a sink to collect events into."""
|
127
|
-
return StateMachineSink(transitions=cls._transition_stats_template.copy())
|
128
|
-
|
129
|
-
def setup(self) -> None:
|
130
|
-
"""Hook method that runs unconditionally in the beginning of each test scenario.
|
131
|
-
|
132
|
-
Does nothing by default.
|
133
|
-
"""
|
134
|
-
|
135
|
-
def teardown(self) -> None:
|
136
|
-
pass
|
137
|
-
|
138
|
-
# To provide the return type in the rendered documentation
|
139
|
-
teardown.__doc__ = RuleBasedStateMachine.teardown.__doc__
|
140
|
-
|
141
|
-
def transform(self, result: StepResult, direction: Direction, case: Case) -> Case:
|
142
|
-
raise NotImplementedError
|
143
|
-
|
144
|
-
def _step(self, case: Case, previous: StepResult | None = None, link: Direction | None = None) -> StepResult | None:
|
145
|
-
# This method is a proxy that is used under the hood during the state machine initialization.
|
146
|
-
# The whole point of having it is to make it possible to override `step`; otherwise, custom "step" is ignored.
|
147
|
-
# It happens because, at the point of initialization, the final class is not yet created.
|
148
|
-
__tracebackhide__ = True
|
149
|
-
if previous is not None and link is not None:
|
150
|
-
return self.step(case, (previous, link))
|
151
|
-
return self.step(case, None)
|
152
|
-
|
153
|
-
def step(self, case: Case, previous: tuple[StepResult, Direction] | None = None) -> StepResult | None:
|
154
|
-
"""A single state machine step.
|
155
|
-
|
156
|
-
:param Case case: Generated test case data that should be sent in an API call to the tested API operation.
|
157
|
-
:param previous: Optional result from the previous step and the direction in which this step should be done.
|
158
|
-
|
159
|
-
Schemathesis prepares data, makes a call and validates the received response.
|
160
|
-
It is the most high-level point to extend the testing process. You probably don't need it in most cases.
|
161
|
-
"""
|
162
|
-
from ..specs.openapi.checks import use_after_free
|
163
|
-
|
164
|
-
__tracebackhide__ = True
|
165
|
-
if previous is not None:
|
166
|
-
result, direction = previous
|
167
|
-
case = self.transform(result, direction, case)
|
168
|
-
self.before_call(case)
|
169
|
-
kwargs = self.get_call_kwargs(case)
|
170
|
-
start = time.monotonic()
|
171
|
-
response = self.call(case, **kwargs)
|
172
|
-
self._transport_kwargs = kwargs
|
173
|
-
elapsed = time.monotonic() - start
|
174
|
-
self.after_call(response, case)
|
175
|
-
self.validate_response(response, case, additional_checks=(use_after_free,))
|
176
|
-
return self.store_result(response, case, elapsed)
|
177
|
-
|
178
|
-
def before_call(self, case: Case) -> None:
|
179
|
-
"""Hook method for modifying the case data before making a request.
|
180
|
-
|
181
|
-
:param Case case: Generated test case data that should be sent in an API call to the tested API operation.
|
182
|
-
|
183
|
-
Use it if you want to inject static data, for example,
|
184
|
-
a query parameter that should always be used in API calls:
|
185
|
-
|
186
|
-
.. code-block:: python
|
187
|
-
|
188
|
-
class APIWorkflow(schema.as_state_machine()):
|
189
|
-
def before_call(self, case):
|
190
|
-
case.query = case.query or {}
|
191
|
-
case.query["test"] = "true"
|
192
|
-
|
193
|
-
You can also modify data only for some operations:
|
194
|
-
|
195
|
-
.. code-block:: python
|
196
|
-
|
197
|
-
class APIWorkflow(schema.as_state_machine()):
|
198
|
-
def before_call(self, case):
|
199
|
-
if case.method == "PUT" and case.path == "/items":
|
200
|
-
case.body["is_fake"] = True
|
201
|
-
"""
|
202
|
-
|
203
|
-
def after_call(self, response: GenericResponse, case: Case) -> None:
|
204
|
-
"""Hook method for additional actions with case or response instances.
|
205
|
-
|
206
|
-
:param response: Response from the application under test.
|
207
|
-
:param Case case: Generated test case data that should be sent in an API call to the tested API operation.
|
208
|
-
|
209
|
-
For example, you can log all response statuses by using this hook:
|
210
|
-
|
211
|
-
.. code-block:: python
|
212
|
-
|
213
|
-
import logging
|
214
|
-
|
215
|
-
logger = logging.getLogger(__file__)
|
216
|
-
logger.setLevel(logging.INFO)
|
217
|
-
|
218
|
-
|
219
|
-
class APIWorkflow(schema.as_state_machine()):
|
220
|
-
def after_call(self, response, case):
|
221
|
-
logger.info(
|
222
|
-
"%s %s -> %d",
|
223
|
-
case.method,
|
224
|
-
case.path,
|
225
|
-
response.status_code,
|
226
|
-
)
|
227
|
-
|
228
|
-
|
229
|
-
# POST /users/ -> 201
|
230
|
-
# GET /users/{user_id} -> 200
|
231
|
-
# PATCH /users/{user_id} -> 200
|
232
|
-
# GET /users/{user_id} -> 200
|
233
|
-
# PATCH /users/{user_id} -> 500
|
234
|
-
"""
|
235
|
-
|
236
|
-
def call(self, case: Case, **kwargs: Any) -> GenericResponse:
|
237
|
-
"""Make a request to the API.
|
238
|
-
|
239
|
-
:param Case case: Generated test case data that should be sent in an API call to the tested API operation.
|
240
|
-
:param kwargs: Keyword arguments that will be passed to the appropriate ``case.call_*`` method.
|
241
|
-
:return: Response from the application under test.
|
242
|
-
|
243
|
-
Note that WSGI/ASGI applications are detected automatically in this method. Depending on the result of this
|
244
|
-
detection the state machine will call the ``call`` method.
|
245
|
-
|
246
|
-
Usually, you don't need to override this method unless you are building a different state machine on top of this
|
247
|
-
one and want to customize the transport layer itself.
|
248
|
-
"""
|
249
|
-
return case.call(**kwargs)
|
250
|
-
|
251
|
-
def get_call_kwargs(self, case: Case) -> dict[str, Any]:
|
252
|
-
"""Create custom keyword arguments that will be passed to the :meth:`Case.call` method.
|
253
|
-
|
254
|
-
Mostly they are proxied to the :func:`requests.request` call.
|
255
|
-
|
256
|
-
:param Case case: Generated test case data that should be sent in an API call to the tested API operation.
|
257
|
-
|
258
|
-
.. code-block:: python
|
259
|
-
|
260
|
-
class APIWorkflow(schema.as_state_machine()):
|
261
|
-
def get_call_kwargs(self, case):
|
262
|
-
return {"verify": False}
|
263
|
-
|
264
|
-
The above example disables the server's TLS certificate verification.
|
265
|
-
"""
|
266
|
-
return {}
|
267
|
-
|
268
|
-
def validate_response(
|
269
|
-
self, response: GenericResponse, case: Case, additional_checks: tuple[CheckFunction, ...] = ()
|
270
|
-
) -> None:
|
271
|
-
"""Validate an API response.
|
272
|
-
|
273
|
-
:param response: Response from the application under test.
|
274
|
-
:param Case case: Generated test case data that should be sent in an API call to the tested API operation.
|
275
|
-
:param additional_checks: A list of checks that will be run together with the default ones.
|
276
|
-
:raises CheckFailed: If any of the supplied checks failed.
|
277
|
-
|
278
|
-
If you need to change the default checks or provide custom validation rules, you can do it here.
|
279
|
-
|
280
|
-
.. code-block:: python
|
281
|
-
|
282
|
-
def my_check(response, case):
|
283
|
-
... # some assertions
|
284
|
-
|
285
|
-
|
286
|
-
class APIWorkflow(schema.as_state_machine()):
|
287
|
-
def validate_response(self, response, case):
|
288
|
-
case.validate_response(response, checks=(my_check,))
|
289
|
-
|
290
|
-
The state machine from the example above will execute only the ``my_check`` check instead of all
|
291
|
-
available checks.
|
292
|
-
|
293
|
-
Each check function should accept ``response`` as the first argument and ``case`` as the second one and raise
|
294
|
-
``AssertionError`` if the check fails.
|
295
|
-
|
296
|
-
**Note** that it is preferred to pass check functions as an argument to ``case.validate_response``.
|
297
|
-
In this case, all checks will be executed, and you'll receive a grouped exception that contains results from
|
298
|
-
all provided checks rather than only the first encountered exception.
|
299
|
-
"""
|
300
|
-
__tracebackhide__ = True
|
301
|
-
case.validate_response(response, additional_checks=additional_checks, transport_kwargs=self._transport_kwargs)
|
302
|
-
|
303
|
-
def store_result(self, response: GenericResponse, case: Case, elapsed: float) -> StepResult:
|
304
|
-
return StepResult(response, case, elapsed)
|
305
|
-
|
306
|
-
|
307
|
-
def _print_case(case: Case, kwargs: dict[str, Any]) -> str:
|
308
|
-
from requests.structures import CaseInsensitiveDict
|
309
|
-
|
310
|
-
operation = f"state.schema['{case.operation.path}']['{case.operation.method.upper()}']"
|
311
|
-
headers = case.headers or CaseInsensitiveDict()
|
312
|
-
headers.update(kwargs.get("headers", {}))
|
313
|
-
case.headers = headers
|
314
|
-
data = [
|
315
|
-
f"{name}={getattr(case, name)!r}"
|
316
|
-
for name in ("path_parameters", "headers", "cookies", "query", "body", "media_type")
|
317
|
-
if getattr(case, name) not in (None, NOT_SET)
|
318
|
-
]
|
319
|
-
return f"{operation}.make_case({', '.join(data)})"
|
320
|
-
|
321
|
-
|
322
|
-
class Direction:
|
323
|
-
name: str
|
324
|
-
status_code: str
|
325
|
-
operation: APIOperation
|
326
|
-
|
327
|
-
def set_data(self, case: Case, elapsed: float, **kwargs: Any) -> None:
|
328
|
-
raise NotImplementedError
|
@@ -1,22 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from dataclasses import dataclass
|
4
|
-
from typing import TYPE_CHECKING
|
5
|
-
|
6
|
-
if TYPE_CHECKING:
|
7
|
-
from . import events
|
8
|
-
|
9
|
-
|
10
|
-
@dataclass
|
11
|
-
class TransitionStats:
|
12
|
-
"""Statistic for transitions in a state machine."""
|
13
|
-
|
14
|
-
def consume(self, event: events.StatefulEvent) -> None:
|
15
|
-
raise NotImplementedError
|
16
|
-
|
17
|
-
def copy(self) -> TransitionStats:
|
18
|
-
"""Create a copy of the statistic."""
|
19
|
-
raise NotImplementedError
|
20
|
-
|
21
|
-
def to_formatted_table(self, width: int) -> str:
|
22
|
-
raise NotImplementedError
|
@@ -1,100 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from typing import TYPE_CHECKING
|
4
|
-
|
5
|
-
from ..exceptions import CheckFailed, get_grouped_exception
|
6
|
-
from ..internal.checks import CheckContext
|
7
|
-
|
8
|
-
if TYPE_CHECKING:
|
9
|
-
from ..failures import FailureContext
|
10
|
-
from ..internal.checks import CheckFunction
|
11
|
-
from ..models import Case
|
12
|
-
from ..transports.responses import GenericResponse
|
13
|
-
from .context import RunnerContext
|
14
|
-
|
15
|
-
|
16
|
-
def validate_response(
|
17
|
-
*,
|
18
|
-
response: GenericResponse,
|
19
|
-
case: Case,
|
20
|
-
runner_ctx: RunnerContext,
|
21
|
-
check_ctx: CheckContext,
|
22
|
-
checks: tuple[CheckFunction, ...],
|
23
|
-
additional_checks: tuple[CheckFunction, ...] = (),
|
24
|
-
max_response_time: int | None = None,
|
25
|
-
) -> None:
|
26
|
-
"""Validate the response against the provided checks."""
|
27
|
-
from .._compat import MultipleFailures
|
28
|
-
from ..checks import _make_max_response_time_failure_message
|
29
|
-
from ..failures import ResponseTimeExceeded
|
30
|
-
from ..models import Check, Status
|
31
|
-
|
32
|
-
exceptions: list[CheckFailed | AssertionError] = []
|
33
|
-
check_results = runner_ctx.checks_for_step
|
34
|
-
|
35
|
-
def _on_failure(exc: CheckFailed | AssertionError, message: str, context: FailureContext | None) -> None:
|
36
|
-
exceptions.append(exc)
|
37
|
-
if runner_ctx.is_seen_in_suite(exc):
|
38
|
-
return
|
39
|
-
failed_check = Check(
|
40
|
-
name=name,
|
41
|
-
value=Status.failure,
|
42
|
-
response=response,
|
43
|
-
elapsed=response.elapsed.total_seconds(),
|
44
|
-
example=copied_case,
|
45
|
-
message=message,
|
46
|
-
context=context,
|
47
|
-
request=None,
|
48
|
-
)
|
49
|
-
runner_ctx.add_failed_check(failed_check)
|
50
|
-
check_results.append(failed_check)
|
51
|
-
runner_ctx.mark_as_seen_in_suite(exc)
|
52
|
-
|
53
|
-
def _on_passed(_name: str, _case: Case) -> None:
|
54
|
-
passed_check = Check(
|
55
|
-
name=_name,
|
56
|
-
value=Status.success,
|
57
|
-
response=response,
|
58
|
-
elapsed=response.elapsed.total_seconds(),
|
59
|
-
example=_case,
|
60
|
-
request=None,
|
61
|
-
)
|
62
|
-
check_results.append(passed_check)
|
63
|
-
|
64
|
-
for check in tuple(checks) + tuple(additional_checks):
|
65
|
-
name = check.__name__
|
66
|
-
copied_case = case.partial_deepcopy()
|
67
|
-
try:
|
68
|
-
skip_check = check(check_ctx, response, copied_case)
|
69
|
-
if not skip_check:
|
70
|
-
_on_passed(name, copied_case)
|
71
|
-
except CheckFailed as exc:
|
72
|
-
if runner_ctx.is_seen_in_run(exc):
|
73
|
-
continue
|
74
|
-
_on_failure(exc, str(exc), exc.context)
|
75
|
-
except AssertionError as exc:
|
76
|
-
if runner_ctx.is_seen_in_run(exc):
|
77
|
-
continue
|
78
|
-
_on_failure(exc, str(exc) or f"Custom check failed: `{name}`", None)
|
79
|
-
except MultipleFailures as exc:
|
80
|
-
for subexc in exc.exceptions:
|
81
|
-
if runner_ctx.is_seen_in_run(subexc):
|
82
|
-
continue
|
83
|
-
_on_failure(subexc, str(subexc), subexc.context)
|
84
|
-
|
85
|
-
if max_response_time:
|
86
|
-
elapsed_time = response.elapsed.total_seconds() * 1000
|
87
|
-
if elapsed_time > max_response_time:
|
88
|
-
message = _make_max_response_time_failure_message(elapsed_time, max_response_time)
|
89
|
-
context = ResponseTimeExceeded(message=message, elapsed=elapsed_time, deadline=max_response_time)
|
90
|
-
try:
|
91
|
-
raise AssertionError(message)
|
92
|
-
except AssertionError as _exc:
|
93
|
-
if not runner_ctx.is_seen_in_run(_exc):
|
94
|
-
_on_failure(_exc, message, context)
|
95
|
-
else:
|
96
|
-
_on_passed("max_response_time", case)
|
97
|
-
|
98
|
-
# Raise a grouped exception so Hypothesis can properly deduplicate it against the other failures
|
99
|
-
if exceptions:
|
100
|
-
raise get_grouped_exception(case.operation.verbose_name, *exceptions)(causes=tuple(exceptions))
|
schemathesis/targets.py
DELETED
@@ -1,77 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from dataclasses import dataclass, field
|
4
|
-
from typing import TYPE_CHECKING, Callable
|
5
|
-
|
6
|
-
if TYPE_CHECKING:
|
7
|
-
from .models import Case
|
8
|
-
from .transports.responses import GenericResponse
|
9
|
-
|
10
|
-
|
11
|
-
@dataclass
|
12
|
-
class TargetContext:
|
13
|
-
"""Context for targeted testing.
|
14
|
-
|
15
|
-
:ivar Case case: Generated example that is being processed.
|
16
|
-
:ivar GenericResponse response: API response.
|
17
|
-
:ivar float response_time: API response time.
|
18
|
-
"""
|
19
|
-
|
20
|
-
case: Case
|
21
|
-
response: GenericResponse
|
22
|
-
response_time: float
|
23
|
-
|
24
|
-
|
25
|
-
def response_time(context: TargetContext) -> float:
|
26
|
-
return context.response_time
|
27
|
-
|
28
|
-
|
29
|
-
Target = Callable[[TargetContext], float]
|
30
|
-
DEFAULT_TARGETS = ()
|
31
|
-
OPTIONAL_TARGETS = (response_time,)
|
32
|
-
ALL_TARGETS: tuple[Target, ...] = DEFAULT_TARGETS + OPTIONAL_TARGETS
|
33
|
-
|
34
|
-
|
35
|
-
@dataclass
|
36
|
-
class TargetMetricCollector:
|
37
|
-
"""Collect multiple observations for target metrics."""
|
38
|
-
|
39
|
-
targets: list[Target]
|
40
|
-
observations: dict[str, list[int | float]] = field(init=False)
|
41
|
-
|
42
|
-
def __post_init__(self) -> None:
|
43
|
-
self.observations = {target.__name__: [] for target in self.targets}
|
44
|
-
|
45
|
-
def reset(self) -> None:
|
46
|
-
"""Reset all collected observations."""
|
47
|
-
for target in self.targets:
|
48
|
-
self.observations[target.__name__].clear()
|
49
|
-
|
50
|
-
def store(self, case: Case, response: GenericResponse) -> None:
|
51
|
-
"""Calculate target metrics & store them."""
|
52
|
-
context = TargetContext(case=case, response=response, response_time=response.elapsed.total_seconds())
|
53
|
-
for target in self.targets:
|
54
|
-
self.observations[target.__name__].append(target(context))
|
55
|
-
|
56
|
-
def maximize(self) -> None:
|
57
|
-
"""Give feedback to the Hypothesis engine, so it maximizes the aggregated metrics."""
|
58
|
-
import hypothesis
|
59
|
-
|
60
|
-
for target in self.targets:
|
61
|
-
# Currently aggregation is just a sum
|
62
|
-
metric = sum(self.observations[target.__name__])
|
63
|
-
hypothesis.target(metric, label=target.__name__)
|
64
|
-
|
65
|
-
|
66
|
-
def register(target: Target) -> Target:
|
67
|
-
"""Register a new testing target for schemathesis CLI.
|
68
|
-
|
69
|
-
:param target: A function that will be called to calculate a metric passed to ``hypothesis.target``.
|
70
|
-
"""
|
71
|
-
from . import cli
|
72
|
-
|
73
|
-
global ALL_TARGETS
|
74
|
-
|
75
|
-
ALL_TARGETS += (target,)
|
76
|
-
cli.TARGETS_TYPE.choices += (target.__name__,) # type: ignore
|
77
|
-
return target
|