schemathesis 3.39.7__py3-none-any.whl → 4.0.0a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +27 -65
- schemathesis/auths.py +26 -68
- schemathesis/checks.py +130 -60
- schemathesis/cli/__init__.py +5 -2105
- 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 +30 -0
- schemathesis/cli/commands/run/executor.py +141 -0
- schemathesis/cli/commands/run/filters.py +202 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/{cassettes.py → commands/run/handlers/cassettes.py} +178 -247
- schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
- schemathesis/cli/commands/run/handlers/output.py +1368 -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} +59 -175
- schemathesis/cli/constants.py +5 -58
- 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} +37 -16
- 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 -7
- 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 +315 -0
- schemathesis/core/fs.py +19 -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/{internal/output.py → core/output/__init__.py} +1 -0
- schemathesis/core/output/sanitization.py +197 -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 +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 +243 -0
- schemathesis/engine/phases/__init__.py +66 -0
- schemathesis/{runner → engine/phases}/probes.py +49 -68
- schemathesis/engine/phases/stateful/__init__.py +66 -0
- schemathesis/engine/phases/stateful/_executor.py +301 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +175 -0
- schemathesis/engine/phases/unit/_executor.py +322 -0
- schemathesis/engine/phases/unit/_pool.py +74 -0
- schemathesis/engine/recorder.py +246 -0
- schemathesis/errors.py +31 -0
- schemathesis/experimental/__init__.py +9 -40
- schemathesis/filters.py +7 -95
- schemathesis/generation/__init__.py +3 -3
- schemathesis/generation/case.py +190 -0
- schemathesis/generation/coverage.py +22 -22
- schemathesis/{_patches.py → generation/hypothesis/__init__.py} +15 -6
- schemathesis/generation/hypothesis/builder.py +585 -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/modes.py +28 -0
- schemathesis/generation/overrides.py +96 -0
- schemathesis/generation/stateful/__init__.py +20 -0
- schemathesis/{stateful → generation/stateful}/state_machine.py +84 -109
- schemathesis/generation/targets.py +69 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +131 -0
- schemathesis/hooks.py +17 -62
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +387 -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} +94 -107
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +456 -228
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +5 -3
- schemathesis/specs/graphql/schemas.py +122 -123
- 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 +97 -134
- schemathesis/specs/openapi/checks.py +238 -219
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +22 -20
- schemathesis/specs/openapi/expressions/__init__.py +11 -15
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/nodes.py +33 -32
- schemathesis/specs/openapi/formats.py +3 -2
- schemathesis/specs/openapi/links.py +123 -299
- schemathesis/specs/openapi/media_types.py +10 -12
- schemathesis/specs/openapi/negative/__init__.py +2 -1
- schemathesis/specs/openapi/negative/mutations.py +3 -2
- schemathesis/specs/openapi/parameters.py +8 -6
- schemathesis/specs/openapi/patterns.py +1 -1
- schemathesis/specs/openapi/references.py +11 -51
- schemathesis/specs/openapi/schemas.py +177 -191
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +10 -6
- schemathesis/specs/openapi/stateful/__init__.py +97 -91
- 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} +69 -7
- schemathesis/transport/wsgi.py +165 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +18 -14
- schemathesis-4.0.0a2.dist-info/RECORD +151 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -559
- schemathesis/_override.py +0 -50
- schemathesis/_rate_limiter.py +0 -7
- 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 -936
- 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 -56
- 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/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -277
- 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 -84
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -38
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- 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 -104
- schemathesis/runner/impl/core.py +0 -1246
- 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/loaders.py +0 -708
- 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/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -359
- 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.7.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.7.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,16 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from functools import reduce
|
4
|
+
from operator import or_
|
5
|
+
from typing import TYPE_CHECKING
|
6
|
+
|
7
|
+
if TYPE_CHECKING:
|
8
|
+
from hypothesis import strategies as st
|
9
|
+
|
10
|
+
|
11
|
+
def combine(strategies: list[st.SearchStrategy] | tuple[st.SearchStrategy]) -> st.SearchStrategy:
|
12
|
+
"""Combine a list of strategies into a single one.
|
13
|
+
|
14
|
+
If the input is `[a, b, c]`, then the result is equivalent to `a | b | c`.
|
15
|
+
"""
|
16
|
+
return reduce(or_, strategies[1:], strategies[0])
|
@@ -0,0 +1,115 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from enum import Enum
|
5
|
+
|
6
|
+
from schemathesis.generation import GenerationMode
|
7
|
+
|
8
|
+
|
9
|
+
class TestPhase(str, Enum):
|
10
|
+
__test__ = False
|
11
|
+
|
12
|
+
EXPLICIT = "explicit"
|
13
|
+
COVERAGE = "coverage"
|
14
|
+
GENERATE = "generate"
|
15
|
+
|
16
|
+
|
17
|
+
class ComponentKind(str, Enum):
|
18
|
+
"""Components that can be generated."""
|
19
|
+
|
20
|
+
QUERY = "query"
|
21
|
+
PATH_PARAMETERS = "path_parameters"
|
22
|
+
HEADERS = "headers"
|
23
|
+
COOKIES = "cookies"
|
24
|
+
BODY = "body"
|
25
|
+
|
26
|
+
|
27
|
+
@dataclass
|
28
|
+
class ComponentInfo:
|
29
|
+
"""Information about how a specific component was generated."""
|
30
|
+
|
31
|
+
mode: GenerationMode
|
32
|
+
|
33
|
+
__slots__ = ("mode",)
|
34
|
+
|
35
|
+
|
36
|
+
@dataclass
|
37
|
+
class GeneratePhaseData:
|
38
|
+
"""Metadata specific to generate phase."""
|
39
|
+
|
40
|
+
|
41
|
+
@dataclass
|
42
|
+
class ExplicitPhaseData:
|
43
|
+
"""Metadata specific to explicit phase."""
|
44
|
+
|
45
|
+
|
46
|
+
@dataclass
|
47
|
+
class CoveragePhaseData:
|
48
|
+
"""Metadata specific to coverage phase."""
|
49
|
+
|
50
|
+
description: str
|
51
|
+
location: str | None
|
52
|
+
parameter: str | None
|
53
|
+
parameter_location: str | None
|
54
|
+
|
55
|
+
__slots__ = ("description", "location", "parameter", "parameter_location")
|
56
|
+
|
57
|
+
|
58
|
+
@dataclass
|
59
|
+
class PhaseInfo:
|
60
|
+
"""Phase-specific information."""
|
61
|
+
|
62
|
+
name: TestPhase
|
63
|
+
data: CoveragePhaseData | ExplicitPhaseData | GeneratePhaseData
|
64
|
+
|
65
|
+
__slots__ = ("name", "data")
|
66
|
+
|
67
|
+
@classmethod
|
68
|
+
def coverage(
|
69
|
+
cls,
|
70
|
+
description: str,
|
71
|
+
location: str | None = None,
|
72
|
+
parameter: str | None = None,
|
73
|
+
parameter_location: str | None = None,
|
74
|
+
) -> PhaseInfo:
|
75
|
+
return cls(
|
76
|
+
name=TestPhase.COVERAGE,
|
77
|
+
data=CoveragePhaseData(
|
78
|
+
description=description, location=location, parameter=parameter, parameter_location=parameter_location
|
79
|
+
),
|
80
|
+
)
|
81
|
+
|
82
|
+
@classmethod
|
83
|
+
def generate(cls) -> PhaseInfo:
|
84
|
+
return cls(name=TestPhase.GENERATE, data=GeneratePhaseData())
|
85
|
+
|
86
|
+
|
87
|
+
@dataclass
|
88
|
+
class GenerationInfo:
|
89
|
+
"""Information about test case generation."""
|
90
|
+
|
91
|
+
time: float
|
92
|
+
mode: GenerationMode
|
93
|
+
|
94
|
+
__slots__ = ("time", "mode")
|
95
|
+
|
96
|
+
|
97
|
+
@dataclass
|
98
|
+
class CaseMetadata:
|
99
|
+
"""Complete metadata for generated cases."""
|
100
|
+
|
101
|
+
generation: GenerationInfo
|
102
|
+
components: dict[ComponentKind, ComponentInfo]
|
103
|
+
phase: PhaseInfo
|
104
|
+
|
105
|
+
__slots__ = ("generation", "components", "phase")
|
106
|
+
|
107
|
+
def __init__(
|
108
|
+
self,
|
109
|
+
generation: GenerationInfo,
|
110
|
+
components: dict[ComponentKind, ComponentInfo],
|
111
|
+
phase: PhaseInfo,
|
112
|
+
) -> None:
|
113
|
+
self.generation = generation
|
114
|
+
self.components = components
|
115
|
+
self.phase = phase
|
@@ -0,0 +1,28 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from enum import Enum
|
4
|
+
|
5
|
+
|
6
|
+
class GenerationMode(str, Enum):
|
7
|
+
"""Defines what data Schemathesis generates for tests."""
|
8
|
+
|
9
|
+
# Generate data, that fits the API schema
|
10
|
+
POSITIVE = "positive"
|
11
|
+
# Doesn't fit the API schema
|
12
|
+
NEGATIVE = "negative"
|
13
|
+
|
14
|
+
@classmethod
|
15
|
+
def default(cls) -> GenerationMode:
|
16
|
+
return cls.POSITIVE
|
17
|
+
|
18
|
+
@classmethod
|
19
|
+
def all(cls) -> list[GenerationMode]:
|
20
|
+
return list(GenerationMode)
|
21
|
+
|
22
|
+
@property
|
23
|
+
def is_positive(self) -> bool:
|
24
|
+
return self == GenerationMode.POSITIVE
|
25
|
+
|
26
|
+
@property
|
27
|
+
def is_negative(self) -> bool:
|
28
|
+
return self == GenerationMode.NEGATIVE
|
@@ -0,0 +1,96 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from collections.abc import Mapping
|
4
|
+
from dataclasses import dataclass
|
5
|
+
from typing import TYPE_CHECKING, Any, Callable
|
6
|
+
|
7
|
+
from schemathesis.core.errors import IncorrectUsage
|
8
|
+
from schemathesis.core.marks import Mark
|
9
|
+
from schemathesis.core.transforms import diff
|
10
|
+
from schemathesis.generation.meta import ComponentKind
|
11
|
+
|
12
|
+
if TYPE_CHECKING:
|
13
|
+
from schemathesis.generation.case import Case
|
14
|
+
from schemathesis.schemas import APIOperation, ParameterSet
|
15
|
+
|
16
|
+
|
17
|
+
@dataclass
|
18
|
+
class Override:
|
19
|
+
"""Overrides for various parts of a test case."""
|
20
|
+
|
21
|
+
query: dict[str, str]
|
22
|
+
headers: dict[str, str]
|
23
|
+
cookies: dict[str, str]
|
24
|
+
path_parameters: dict[str, str]
|
25
|
+
|
26
|
+
def for_operation(self, operation: APIOperation) -> dict[str, dict[str, str]]:
|
27
|
+
return {
|
28
|
+
"query": (_for_parameters(self.query, operation.query)),
|
29
|
+
"headers": (_for_parameters(self.headers, operation.headers)),
|
30
|
+
"cookies": (_for_parameters(self.cookies, operation.cookies)),
|
31
|
+
"path_parameters": (_for_parameters(self.path_parameters, operation.path_parameters)),
|
32
|
+
}
|
33
|
+
|
34
|
+
@classmethod
|
35
|
+
def from_components(cls, components: dict[ComponentKind, StoredValue], case: Case) -> Override:
|
36
|
+
return Override(
|
37
|
+
**{
|
38
|
+
kind.value: get_component_diff(stored=stored, current=getattr(case, kind.value))
|
39
|
+
for kind, stored in components.items()
|
40
|
+
}
|
41
|
+
)
|
42
|
+
|
43
|
+
|
44
|
+
def _for_parameters(overridden: dict[str, str], defined: ParameterSet) -> dict[str, str]:
|
45
|
+
output = {}
|
46
|
+
for param in defined:
|
47
|
+
if param.name in overridden:
|
48
|
+
output[param.name] = overridden[param.name]
|
49
|
+
return output
|
50
|
+
|
51
|
+
|
52
|
+
@dataclass
|
53
|
+
class StoredValue:
|
54
|
+
value: dict[str, Any] | None
|
55
|
+
is_generated: bool
|
56
|
+
|
57
|
+
__slots__ = ("value", "is_generated")
|
58
|
+
|
59
|
+
|
60
|
+
def store_original_state(value: dict[str, Any] | None) -> dict[str, Any] | None:
|
61
|
+
if isinstance(value, Mapping):
|
62
|
+
return value.copy()
|
63
|
+
return value
|
64
|
+
|
65
|
+
|
66
|
+
def get_component_diff(stored: StoredValue, current: dict[str, Any] | None) -> dict[str, Any]:
|
67
|
+
"""Calculate difference between stored and current components."""
|
68
|
+
if not (current and stored.value):
|
69
|
+
return {}
|
70
|
+
if stored.is_generated:
|
71
|
+
return diff(stored.value, current)
|
72
|
+
return current
|
73
|
+
|
74
|
+
|
75
|
+
def store_components(case: Case) -> dict[ComponentKind, StoredValue]:
|
76
|
+
"""Store original component states for a test case."""
|
77
|
+
return {
|
78
|
+
kind: StoredValue(
|
79
|
+
value=store_original_state(getattr(case, kind.value)),
|
80
|
+
is_generated=bool(case.meta and kind in case.meta.components),
|
81
|
+
)
|
82
|
+
for kind in [
|
83
|
+
ComponentKind.QUERY,
|
84
|
+
ComponentKind.HEADERS,
|
85
|
+
ComponentKind.COOKIES,
|
86
|
+
ComponentKind.PATH_PARAMETERS,
|
87
|
+
]
|
88
|
+
}
|
89
|
+
|
90
|
+
|
91
|
+
OverrideMark = Mark[Override](attr_name="override")
|
92
|
+
|
93
|
+
|
94
|
+
def check_no_override_mark(test: Callable) -> None:
|
95
|
+
if OverrideMark.is_set(test):
|
96
|
+
raise IncorrectUsage(f"`{test.__name__}` has already been decorated with `override`.")
|
@@ -0,0 +1,20 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import TYPE_CHECKING
|
4
|
+
|
5
|
+
if TYPE_CHECKING:
|
6
|
+
import hypothesis
|
7
|
+
|
8
|
+
from schemathesis.generation.stateful.state_machine import APIStateMachine
|
9
|
+
|
10
|
+
|
11
|
+
def run_state_machine_as_test(
|
12
|
+
state_machine_factory: type[APIStateMachine], *, settings: hypothesis.settings | None = None
|
13
|
+
) -> None:
|
14
|
+
"""Run a state machine as a test.
|
15
|
+
|
16
|
+
It automatically adds the `_min_steps` argument if ``Hypothesis`` is recent enough.
|
17
|
+
"""
|
18
|
+
from hypothesis.stateful import run_state_machine_as_test as _run_state_machine_as_test
|
19
|
+
|
20
|
+
return _run_state_machine_as_test(state_machine_factory, settings=settings, _min_steps=2)
|
@@ -1,39 +1,86 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import re
|
4
|
-
import time
|
5
4
|
from dataclasses import dataclass
|
6
5
|
from functools import lru_cache
|
7
6
|
from typing import TYPE_CHECKING, Any, ClassVar
|
8
7
|
|
8
|
+
import hypothesis
|
9
9
|
from hypothesis.errors import InvalidDefinition
|
10
10
|
from hypothesis.stateful import RuleBasedStateMachine
|
11
11
|
|
12
|
-
from
|
13
|
-
from
|
14
|
-
from
|
15
|
-
from
|
16
|
-
from
|
17
|
-
from .config import _default_hypothesis_settings_factory
|
18
|
-
from .runner import StatefulTestRunner, StatefulTestRunnerConfig
|
19
|
-
from .sink import StateMachineSink
|
12
|
+
from schemathesis.checks import CheckFunction
|
13
|
+
from schemathesis.core.errors import IncorrectUsage
|
14
|
+
from schemathesis.core.result import Result
|
15
|
+
from schemathesis.core.transport import Response
|
16
|
+
from schemathesis.generation.case import Case
|
20
17
|
|
21
18
|
if TYPE_CHECKING:
|
22
19
|
import hypothesis
|
23
20
|
from requests.structures import CaseInsensitiveDict
|
24
21
|
|
25
|
-
from
|
26
|
-
|
27
|
-
|
22
|
+
from schemathesis.schemas import BaseSchema
|
23
|
+
|
24
|
+
|
25
|
+
NO_LINKS_ERROR_MESSAGE = (
|
26
|
+
"Stateful testing requires at least one OpenAPI link in the schema, but no links detected. "
|
27
|
+
"Please add OpenAPI links to enable stateful testing or use stateless tests instead. \n"
|
28
|
+
"See https://schemathesis.readthedocs.io/en/stable/stateful.html#how-to-specify-connections for more information."
|
29
|
+
)
|
30
|
+
|
31
|
+
DEFAULT_STATE_MACHINE_SETTINGS = hypothesis.settings(
|
32
|
+
phases=[hypothesis.Phase.generate],
|
33
|
+
deadline=None,
|
34
|
+
stateful_step_count=6,
|
35
|
+
suppress_health_check=list(hypothesis.HealthCheck),
|
36
|
+
)
|
37
|
+
|
38
|
+
|
39
|
+
@dataclass
|
40
|
+
class StepInput:
|
41
|
+
"""Input for a single state machine step."""
|
42
|
+
|
43
|
+
case: Case
|
44
|
+
transition: Transition | None # None for initial steps
|
45
|
+
|
46
|
+
__slots__ = ("case", "transition")
|
47
|
+
|
48
|
+
@classmethod
|
49
|
+
def initial(cls, case: Case) -> StepInput:
|
50
|
+
return cls(case=case, transition=None)
|
51
|
+
|
52
|
+
|
53
|
+
@dataclass
|
54
|
+
class Transition:
|
55
|
+
"""Data about transition execution."""
|
56
|
+
|
57
|
+
# ID of the transition (e.g. link name)
|
58
|
+
id: str
|
59
|
+
parent_id: str
|
60
|
+
parameters: dict[str, dict[str, ExtractedParam]]
|
61
|
+
request_body: ExtractedParam | None
|
62
|
+
|
63
|
+
__slots__ = ("id", "parent_id", "parameters", "request_body")
|
64
|
+
|
65
|
+
|
66
|
+
@dataclass
|
67
|
+
class ExtractedParam:
|
68
|
+
"""Result of parameter extraction."""
|
69
|
+
|
70
|
+
definition: Any
|
71
|
+
value: Result[Any, Exception]
|
72
|
+
|
73
|
+
__slots__ = ("definition", "value")
|
28
74
|
|
29
75
|
|
30
76
|
@dataclass
|
31
|
-
class
|
77
|
+
class StepOutput:
|
32
78
|
"""Output from a single transition of a state machine."""
|
33
79
|
|
34
|
-
response:
|
80
|
+
response: Response
|
35
81
|
case: Case
|
36
|
-
|
82
|
+
|
83
|
+
__slots__ = ("response", "case")
|
37
84
|
|
38
85
|
|
39
86
|
def _normalize_name(name: str) -> str:
|
@@ -51,25 +98,23 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
51
98
|
# attribute will be renamed in the future
|
52
99
|
bundles: ClassVar[dict[str, CaseInsensitiveDict]] # type: ignore
|
53
100
|
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
101
|
|
57
102
|
def __init__(self) -> None:
|
58
103
|
try:
|
59
104
|
super().__init__() # type: ignore
|
60
105
|
except InvalidDefinition as exc:
|
61
106
|
if "defines no rules" in str(exc):
|
62
|
-
raise
|
107
|
+
raise IncorrectUsage(NO_LINKS_ERROR_MESSAGE) from None
|
63
108
|
raise
|
64
109
|
self.setup()
|
65
110
|
|
66
111
|
@classmethod
|
67
112
|
@lru_cache
|
68
113
|
def _to_test_case(cls) -> type:
|
69
|
-
from . import run_state_machine_as_test
|
114
|
+
from schemathesis.generation.stateful import run_state_machine_as_test
|
70
115
|
|
71
116
|
class StateMachineTestCase(RuleBasedStateMachine.TestCase):
|
72
|
-
settings =
|
117
|
+
settings = DEFAULT_STATE_MACHINE_SETTINGS
|
73
118
|
|
74
119
|
def runTest(self) -> None:
|
75
120
|
run_state_machine_as_test(cls, settings=self.settings)
|
@@ -80,33 +125,20 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
80
125
|
StateMachineTestCase.__qualname__ = cls.__qualname__ + ".TestCase"
|
81
126
|
return StateMachineTestCase
|
82
127
|
|
83
|
-
def
|
84
|
-
|
85
|
-
|
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:
|
128
|
+
def _new_name(self, target: str) -> str:
|
129
|
+
target = _normalize_name(target)
|
130
|
+
return super()._new_name(target) # type: ignore
|
91
131
|
|
92
|
-
|
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:
|
132
|
+
def _get_target_for_result(self, result: StepOutput) -> str | None:
|
97
133
|
raise NotImplementedError
|
98
134
|
|
99
|
-
def _add_result_to_targets(self, targets: tuple[str, ...], result:
|
135
|
+
def _add_result_to_targets(self, targets: tuple[str, ...], result: StepOutput | None) -> None:
|
100
136
|
if result is None:
|
101
137
|
return
|
102
138
|
target = self._get_target_for_result(result)
|
103
139
|
if target is not None:
|
104
140
|
super()._add_result_to_targets((target,), result)
|
105
141
|
|
106
|
-
@classmethod
|
107
|
-
def format_rules(cls) -> str:
|
108
|
-
raise NotImplementedError
|
109
|
-
|
110
142
|
@classmethod
|
111
143
|
def run(cls, *, settings: hypothesis.settings | None = None) -> None:
|
112
144
|
"""Run state machine as a test."""
|
@@ -114,23 +146,8 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
114
146
|
|
115
147
|
return run_state_machine_as_test(cls, settings=settings)
|
116
148
|
|
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
149
|
def setup(self) -> None:
|
130
|
-
"""Hook method that runs unconditionally in the beginning of each test scenario.
|
131
|
-
|
132
|
-
Does nothing by default.
|
133
|
-
"""
|
150
|
+
"""Hook method that runs unconditionally in the beginning of each test scenario."""
|
134
151
|
|
135
152
|
def teardown(self) -> None:
|
136
153
|
pass
|
@@ -138,19 +155,11 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
138
155
|
# To provide the return type in the rendered documentation
|
139
156
|
teardown.__doc__ = RuleBasedStateMachine.teardown.__doc__
|
140
157
|
|
141
|
-
def
|
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.
|
158
|
+
def _step(self, input: StepInput) -> StepOutput | None:
|
148
159
|
__tracebackhide__ = True
|
149
|
-
|
150
|
-
return self.step(case, (previous, link))
|
151
|
-
return self.step(case, None)
|
160
|
+
return self.step(input)
|
152
161
|
|
153
|
-
def step(self,
|
162
|
+
def step(self, input: StepInput) -> StepOutput:
|
154
163
|
"""A single state machine step.
|
155
164
|
|
156
165
|
:param Case case: Generated test case data that should be sent in an API call to the tested API operation.
|
@@ -159,20 +168,13 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
159
168
|
Schemathesis prepares data, makes a call and validates the received response.
|
160
169
|
It is the most high-level point to extend the testing process. You probably don't need it in most cases.
|
161
170
|
"""
|
162
|
-
from ..specs.openapi.checks import use_after_free
|
163
|
-
|
164
171
|
__tracebackhide__ = True
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
self.
|
169
|
-
|
170
|
-
|
171
|
-
response = self.call(case, **kwargs)
|
172
|
-
elapsed = time.monotonic() - start
|
173
|
-
self.after_call(response, case)
|
174
|
-
self.validate_response(response, case, additional_checks=(use_after_free,))
|
175
|
-
return self.store_result(response, case, elapsed)
|
172
|
+
self.before_call(input.case)
|
173
|
+
kwargs = self.get_call_kwargs(input.case)
|
174
|
+
response = self.call(input.case, **kwargs)
|
175
|
+
self.after_call(response, input.case)
|
176
|
+
self.validate_response(response, input.case)
|
177
|
+
return StepOutput(response, input.case)
|
176
178
|
|
177
179
|
def before_call(self, case: Case) -> None:
|
178
180
|
"""Hook method for modifying the case data before making a request.
|
@@ -199,7 +201,7 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
199
201
|
case.body["is_fake"] = True
|
200
202
|
"""
|
201
203
|
|
202
|
-
def after_call(self, response:
|
204
|
+
def after_call(self, response: Response, case: Case) -> None:
|
203
205
|
"""Hook method for additional actions with case or response instances.
|
204
206
|
|
205
207
|
:param response: Response from the application under test.
|
@@ -232,7 +234,7 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
232
234
|
# PATCH /users/{user_id} -> 500
|
233
235
|
"""
|
234
236
|
|
235
|
-
def call(self, case: Case, **kwargs: Any) ->
|
237
|
+
def call(self, case: Case, **kwargs: Any) -> Response:
|
236
238
|
"""Make a request to the API.
|
237
239
|
|
238
240
|
:param Case case: Generated test case data that should be sent in an API call to the tested API operation.
|
@@ -265,14 +267,14 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
265
267
|
return {}
|
266
268
|
|
267
269
|
def validate_response(
|
268
|
-
self, response:
|
270
|
+
self, response: Response, case: Case, additional_checks: list[CheckFunction] | None = None
|
269
271
|
) -> None:
|
270
272
|
"""Validate an API response.
|
271
273
|
|
272
274
|
:param response: Response from the application under test.
|
273
275
|
:param Case case: Generated test case data that should be sent in an API call to the tested API operation.
|
274
276
|
:param additional_checks: A list of checks that will be run together with the default ones.
|
275
|
-
:raises
|
277
|
+
:raises FailureGroup: If any of the supplied checks failed.
|
276
278
|
|
277
279
|
If you need to change the default checks or provide custom validation rules, you can do it here.
|
278
280
|
|
@@ -298,30 +300,3 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
298
300
|
"""
|
299
301
|
__tracebackhide__ = True
|
300
302
|
case.validate_response(response, additional_checks=additional_checks)
|
301
|
-
|
302
|
-
def store_result(self, response: GenericResponse, case: Case, elapsed: float) -> StepResult:
|
303
|
-
return StepResult(response, case, elapsed)
|
304
|
-
|
305
|
-
|
306
|
-
def _print_case(case: Case, kwargs: dict[str, Any]) -> str:
|
307
|
-
from requests.structures import CaseInsensitiveDict
|
308
|
-
|
309
|
-
operation = f"state.schema['{case.operation.path}']['{case.operation.method.upper()}']"
|
310
|
-
headers = case.headers or CaseInsensitiveDict()
|
311
|
-
headers.update(kwargs.get("headers", {}))
|
312
|
-
case.headers = headers
|
313
|
-
data = [
|
314
|
-
f"{name}={getattr(case, name)!r}"
|
315
|
-
for name in ("path_parameters", "headers", "cookies", "query", "body", "media_type")
|
316
|
-
if getattr(case, name) not in (None, NOT_SET)
|
317
|
-
]
|
318
|
-
return f"{operation}.make_case({', '.join(data)})"
|
319
|
-
|
320
|
-
|
321
|
-
class Direction:
|
322
|
-
name: str
|
323
|
-
status_code: str
|
324
|
-
operation: APIOperation
|
325
|
-
|
326
|
-
def set_data(self, case: Case, elapsed: float, **kwargs: Any) -> None:
|
327
|
-
raise NotImplementedError
|
@@ -0,0 +1,69 @@
|
|
1
|
+
"""Support for Targeted Property-Based Testing."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from dataclasses import dataclass
|
6
|
+
from typing import Callable, Sequence
|
7
|
+
|
8
|
+
from schemathesis.core.registries import Registry
|
9
|
+
from schemathesis.core.transport import Response
|
10
|
+
from schemathesis.generation.case import Case
|
11
|
+
|
12
|
+
|
13
|
+
@dataclass
|
14
|
+
class TargetContext:
|
15
|
+
case: Case
|
16
|
+
response: Response
|
17
|
+
|
18
|
+
__slots__ = ("case", "response")
|
19
|
+
|
20
|
+
|
21
|
+
TargetFunction = Callable[[TargetContext], float]
|
22
|
+
|
23
|
+
TARGETS = Registry[TargetFunction]()
|
24
|
+
target = TARGETS.register
|
25
|
+
|
26
|
+
|
27
|
+
@target
|
28
|
+
def response_time(ctx: TargetContext) -> float:
|
29
|
+
"""Response time as a metric to maximize."""
|
30
|
+
return ctx.response.elapsed
|
31
|
+
|
32
|
+
|
33
|
+
class TargetMetricCollector:
|
34
|
+
"""Collect multiple observations for target metrics."""
|
35
|
+
|
36
|
+
__slots__ = ("targets", "observations")
|
37
|
+
|
38
|
+
def __init__(self, targets: list[TargetFunction] | None = None) -> None:
|
39
|
+
self.targets = targets or []
|
40
|
+
self.observations: dict[str, list[float]] = {target.__name__: [] for target in self.targets}
|
41
|
+
|
42
|
+
def reset(self) -> None:
|
43
|
+
"""Reset all collected observations."""
|
44
|
+
for target in self.targets:
|
45
|
+
self.observations[target.__name__].clear()
|
46
|
+
|
47
|
+
def store(self, case: Case, response: Response) -> None:
|
48
|
+
"""Calculate target metrics & store them."""
|
49
|
+
context = TargetContext(case=case, response=response)
|
50
|
+
for target in self.targets:
|
51
|
+
self.observations[target.__name__].append(target(context))
|
52
|
+
|
53
|
+
def maximize(self) -> None:
|
54
|
+
"""Give feedback to the Hypothesis engine, so it maximizes the aggregated metrics."""
|
55
|
+
import hypothesis
|
56
|
+
|
57
|
+
for target in self.targets:
|
58
|
+
# Currently aggregation is just a sum
|
59
|
+
metric = sum(self.observations[target.__name__])
|
60
|
+
hypothesis.target(metric, label=target.__name__)
|
61
|
+
|
62
|
+
|
63
|
+
def run(targets: Sequence[TargetFunction], case: Case, response: Response) -> None:
|
64
|
+
import hypothesis
|
65
|
+
|
66
|
+
context = TargetContext(case=case, response=response)
|
67
|
+
for target in targets:
|
68
|
+
value = target(context)
|
69
|
+
hypothesis.target(value, label=target.__name__)
|
@@ -0,0 +1,15 @@
|
|
1
|
+
from schemathesis.graphql.loaders import from_asgi, from_dict, from_file, from_path, from_url, from_wsgi
|
2
|
+
|
3
|
+
from ..specs.graphql import nodes
|
4
|
+
from ..specs.graphql.scalars import scalar
|
5
|
+
|
6
|
+
__all__ = [
|
7
|
+
"from_url",
|
8
|
+
"from_asgi",
|
9
|
+
"from_wsgi",
|
10
|
+
"from_file",
|
11
|
+
"from_path",
|
12
|
+
"from_dict",
|
13
|
+
"nodes",
|
14
|
+
"scalar",
|
15
|
+
]
|