schemathesis 3.39.15__py3-none-any.whl → 4.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +41 -79
- schemathesis/auths.py +111 -122
- schemathesis/checks.py +169 -60
- schemathesis/cli/__init__.py +15 -2117
- schemathesis/cli/commands/__init__.py +85 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +590 -0
- schemathesis/cli/commands/run/context.py +204 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
- schemathesis/cli/commands/run/handlers/output.py +1628 -0
- schemathesis/cli/commands/run/loaders.py +114 -0
- schemathesis/cli/commands/run/validation.py +246 -0
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +84 -0
- schemathesis/cli/{options.py → ext/options.py} +36 -34
- schemathesis/config/__init__.py +189 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +99 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +156 -0
- schemathesis/config/_generation.py +149 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +327 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +187 -0
- schemathesis/config/_projects.py +527 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +885 -0
- schemathesis/core/__init__.py +67 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +459 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/{throttling.py → core/rate_limit.py} +16 -17
- schemathesis/core/registries.py +31 -0
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +54 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +118 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +169 -0
- schemathesis/engine/errors.py +464 -0
- schemathesis/engine/events.py +258 -0
- schemathesis/engine/phases/__init__.py +88 -0
- schemathesis/{runner → engine/phases}/probes.py +52 -68
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +356 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +212 -0
- schemathesis/engine/phases/unit/_executor.py +416 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +247 -0
- schemathesis/errors.py +43 -0
- schemathesis/filters.py +17 -98
- schemathesis/generation/__init__.py +5 -33
- schemathesis/generation/case.py +317 -0
- schemathesis/generation/coverage.py +282 -175
- schemathesis/generation/hypothesis/__init__.py +36 -0
- schemathesis/generation/hypothesis/builder.py +800 -0
- schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +116 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +278 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +284 -0
- schemathesis/hooks.py +80 -101
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +455 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +313 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +281 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -273
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +42 -6
- schemathesis/specs/graphql/schemas.py +141 -137
- schemathesis/specs/graphql/validation.py +11 -17
- schemathesis/specs/openapi/__init__.py +6 -1
- schemathesis/specs/openapi/_cache.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +142 -156
- schemathesis/specs/openapi/checks.py +368 -257
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +23 -21
- schemathesis/specs/openapi/expressions/__init__.py +31 -19
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/lexer.py +1 -1
- schemathesis/specs/openapi/expressions/nodes.py +36 -41
- schemathesis/specs/openapi/expressions/parser.py +1 -1
- schemathesis/specs/openapi/formats.py +35 -7
- schemathesis/specs/openapi/media_types.py +53 -12
- schemathesis/specs/openapi/negative/__init__.py +7 -4
- schemathesis/specs/openapi/negative/mutations.py +6 -5
- schemathesis/specs/openapi/parameters.py +7 -10
- schemathesis/specs/openapi/patterns.py +94 -31
- schemathesis/specs/openapi/references.py +12 -53
- schemathesis/specs/openapi/schemas.py +238 -308
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +12 -6
- schemathesis/specs/openapi/stateful/__init__.py +268 -133
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/links.py +209 -0
- schemathesis/transport/__init__.py +142 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +124 -0
- schemathesis/transport/requests.py +244 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -11
- schemathesis/transport/wsgi.py +171 -0
- schemathesis-4.0.0.dist-info/METADATA +204 -0
- schemathesis-4.0.0.dist-info/RECORD +164 -0
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -712
- schemathesis/_override.py +0 -50
- schemathesis/_patches.py +0 -21
- schemathesis/_rate_limiter.py +0 -7
- schemathesis/cli/callbacks.py +0 -466
- schemathesis/cli/cassettes.py +0 -561
- schemathesis/cli/context.py +0 -75
- schemathesis/cli/debug.py +0 -27
- schemathesis/cli/handlers.py +0 -19
- schemathesis/cli/junitxml.py +0 -124
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -920
- schemathesis/cli/output/short.py +0 -59
- schemathesis/cli/reporting.py +0 -79
- schemathesis/cli/sanitization.py +0 -26
- schemathesis/code_samples.py +0 -151
- schemathesis/constants.py +0 -54
- schemathesis/contrib/__init__.py +0 -11
- schemathesis/contrib/openapi/__init__.py +0 -11
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -16
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -571
- schemathesis/experimental/__init__.py +0 -109
- schemathesis/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -284
- schemathesis/fixups/__init__.py +0 -37
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -28
- schemathesis/generation/_methods.py +0 -44
- schemathesis/graphql.py +0 -3
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/checks.py +0 -86
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -37
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- schemathesis/internal/output.py +0 -68
- schemathesis/internal/transformation.py +0 -26
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -474
- schemathesis/loaders.py +0 -122
- schemathesis/models.py +0 -1341
- schemathesis/parameters.py +0 -90
- schemathesis/runner/__init__.py +0 -605
- schemathesis/runner/events.py +0 -389
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/context.py +0 -88
- schemathesis/runner/impl/core.py +0 -1280
- schemathesis/runner/impl/solo.py +0 -80
- schemathesis/runner/impl/threadpool.py +0 -391
- schemathesis/runner/serialization.py +0 -544
- schemathesis/sanitization.py +0 -252
- schemathesis/serializers.py +0 -328
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -202
- schemathesis/service/client.py +0 -133
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -61
- schemathesis/service/extensions.py +0 -224
- schemathesis/service/hosts.py +0 -111
- schemathesis/service/metadata.py +0 -71
- schemathesis/service/models.py +0 -258
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -173
- schemathesis/service/usage.py +0 -66
- schemathesis/specs/graphql/loaders.py +0 -364
- schemathesis/specs/openapi/expressions/context.py +0 -16
- schemathesis/specs/openapi/links.py +0 -389
- schemathesis/specs/openapi/loaders.py +0 -707
- schemathesis/specs/openapi/stateful/statistic.py +0 -198
- schemathesis/specs/openapi/stateful/types.py +0 -14
- schemathesis/specs/openapi/validation.py +0 -26
- schemathesis/stateful/__init__.py +0 -147
- schemathesis/stateful/config.py +0 -97
- schemathesis/stateful/context.py +0 -135
- schemathesis/stateful/events.py +0 -274
- schemathesis/stateful/runner.py +0 -309
- schemathesis/stateful/sink.py +0 -68
- schemathesis/stateful/state_machine.py +0 -328
- schemathesis/stateful/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -369
- schemathesis/transports/asgi.py +0 -7
- schemathesis/transports/auth.py +0 -38
- schemathesis/transports/headers.py +0 -36
- schemathesis/transports/responses.py +0 -57
- schemathesis/types.py +0 -44
- schemathesis/utils.py +0 -164
- schemathesis-3.39.15.dist-info/METADATA +0 -293
- schemathesis-3.39.15.dist-info/RECORD +0 -160
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
- /schemathesis/{internal → core}/result.py +0 -0
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -1,198 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from dataclasses import dataclass, field
|
4
|
-
from typing import TYPE_CHECKING, Iterator, Union
|
5
|
-
|
6
|
-
from ....internal.copy import fast_deepcopy
|
7
|
-
from ....stateful.statistic import TransitionStats
|
8
|
-
|
9
|
-
if TYPE_CHECKING:
|
10
|
-
from ....stateful import events
|
11
|
-
from .types import AggregatedResponseCounter, LinkName, ResponseCounter, SourceName, StatusCode, TargetName
|
12
|
-
|
13
|
-
|
14
|
-
@dataclass
|
15
|
-
class LinkSource:
|
16
|
-
name: str
|
17
|
-
responses: dict[StatusCode, dict[TargetName, dict[LinkName, ResponseCounter]]]
|
18
|
-
is_first: bool
|
19
|
-
|
20
|
-
__slots__ = ("name", "responses", "is_first")
|
21
|
-
|
22
|
-
|
23
|
-
@dataclass
|
24
|
-
class OperationResponse:
|
25
|
-
status_code: str
|
26
|
-
targets: dict[TargetName, dict[LinkName, ResponseCounter]]
|
27
|
-
is_last: bool
|
28
|
-
|
29
|
-
__slots__ = ("status_code", "targets", "is_last")
|
30
|
-
|
31
|
-
|
32
|
-
@dataclass
|
33
|
-
class Link:
|
34
|
-
name: str
|
35
|
-
target: str
|
36
|
-
responses: ResponseCounter
|
37
|
-
is_last: bool
|
38
|
-
is_single: bool
|
39
|
-
|
40
|
-
__slots__ = ("name", "target", "responses", "is_last", "is_single")
|
41
|
-
|
42
|
-
|
43
|
-
StatisticEntry = Union[LinkSource, OperationResponse, Link]
|
44
|
-
|
45
|
-
|
46
|
-
@dataclass
|
47
|
-
class FormattedStatisticEntry:
|
48
|
-
line: str
|
49
|
-
entry: StatisticEntry
|
50
|
-
__slots__ = ("line", "entry")
|
51
|
-
|
52
|
-
|
53
|
-
@dataclass
|
54
|
-
class OpenAPILinkStats(TransitionStats):
|
55
|
-
"""Statistics about link transitions for a state machine run."""
|
56
|
-
|
57
|
-
transitions: dict[SourceName, dict[StatusCode, dict[TargetName, dict[LinkName, ResponseCounter]]]]
|
58
|
-
|
59
|
-
roots: dict[TargetName, ResponseCounter] = field(default_factory=dict)
|
60
|
-
|
61
|
-
__slots__ = ("transitions",)
|
62
|
-
|
63
|
-
def consume(self, event: events.StatefulEvent) -> None:
|
64
|
-
from ....stateful import events
|
65
|
-
|
66
|
-
if isinstance(event, events.StepFinished):
|
67
|
-
if event.transition_id is not None:
|
68
|
-
transition_id = event.transition_id
|
69
|
-
source = self.transitions[transition_id.source]
|
70
|
-
transition = source[transition_id.status_code][event.target][transition_id.name]
|
71
|
-
if event.response is not None:
|
72
|
-
key = event.response.status_code
|
73
|
-
else:
|
74
|
-
key = None
|
75
|
-
counter = transition.setdefault(key, 0)
|
76
|
-
transition[key] = counter + 1
|
77
|
-
else:
|
78
|
-
# A start of a sequence has an empty source and does not belong to any transition
|
79
|
-
target = self.roots.setdefault(event.target, {})
|
80
|
-
if event.response is not None:
|
81
|
-
key = event.response.status_code
|
82
|
-
else:
|
83
|
-
key = None
|
84
|
-
counter = target.setdefault(key, 0)
|
85
|
-
target[key] = counter + 1
|
86
|
-
|
87
|
-
def copy(self) -> OpenAPILinkStats:
|
88
|
-
return self.__class__(transitions=fast_deepcopy(self.transitions))
|
89
|
-
|
90
|
-
def iter(self) -> Iterator[StatisticEntry]:
|
91
|
-
for source_idx, (source, responses) in enumerate(self.transitions.items()):
|
92
|
-
yield LinkSource(name=source, responses=responses, is_first=source_idx == 0)
|
93
|
-
for response_idx, (status_code, targets) in enumerate(responses.items()):
|
94
|
-
yield OperationResponse(
|
95
|
-
status_code=status_code, targets=targets, is_last=response_idx == len(responses) - 1
|
96
|
-
)
|
97
|
-
for target_idx, (target, links) in enumerate(targets.items()):
|
98
|
-
for link_idx, (link_name, link_responses) in enumerate(links.items()):
|
99
|
-
yield Link(
|
100
|
-
name=link_name,
|
101
|
-
target=target,
|
102
|
-
responses=link_responses,
|
103
|
-
is_last=target_idx == len(targets) - 1 and link_idx == len(links) - 1,
|
104
|
-
is_single=len(links) == 1,
|
105
|
-
)
|
106
|
-
|
107
|
-
def iter_with_format(self) -> Iterator[FormattedStatisticEntry]:
|
108
|
-
current_response = None
|
109
|
-
for entry in self.iter():
|
110
|
-
if isinstance(entry, LinkSource):
|
111
|
-
if not entry.is_first:
|
112
|
-
yield FormattedStatisticEntry(line=f"\n{entry.name}", entry=entry)
|
113
|
-
else:
|
114
|
-
yield FormattedStatisticEntry(line=f"{entry.name}", entry=entry)
|
115
|
-
elif isinstance(entry, OperationResponse):
|
116
|
-
current_response = entry
|
117
|
-
if entry.is_last:
|
118
|
-
yield FormattedStatisticEntry(line=f"└── {entry.status_code}", entry=entry)
|
119
|
-
else:
|
120
|
-
yield FormattedStatisticEntry(line=f"├── {entry.status_code}", entry=entry)
|
121
|
-
else:
|
122
|
-
if current_response is not None and current_response.is_last:
|
123
|
-
line = " "
|
124
|
-
else:
|
125
|
-
line = "│ "
|
126
|
-
if entry.is_last:
|
127
|
-
line += "└"
|
128
|
-
else:
|
129
|
-
line += "├"
|
130
|
-
if entry.is_single or entry.name == entry.target:
|
131
|
-
line += f"── {entry.target}"
|
132
|
-
else:
|
133
|
-
line += f"── {entry.name} -> {entry.target}"
|
134
|
-
yield FormattedStatisticEntry(line=line, entry=entry)
|
135
|
-
|
136
|
-
def to_formatted_table(self, width: int) -> str:
|
137
|
-
"""Format the statistic as a table."""
|
138
|
-
entries = list(self.iter_with_format())
|
139
|
-
lines: list[str | list[str]] = [HEADER, ""]
|
140
|
-
column_widths = [len(column) for column in HEADER]
|
141
|
-
for entry in entries:
|
142
|
-
if isinstance(entry.entry, Link):
|
143
|
-
aggregated = _aggregate_responses(entry.entry.responses)
|
144
|
-
values = [
|
145
|
-
entry.line,
|
146
|
-
str(aggregated["2xx"]),
|
147
|
-
str(aggregated["4xx"]),
|
148
|
-
str(aggregated["5xx"]),
|
149
|
-
str(aggregated["Total"]),
|
150
|
-
]
|
151
|
-
column_widths = [max(column_widths[idx], len(column)) for idx, column in enumerate(values)]
|
152
|
-
lines.append(values)
|
153
|
-
else:
|
154
|
-
lines.append(entry.line)
|
155
|
-
used_width = sum(column_widths) + 4 * PADDING
|
156
|
-
max_space = width - used_width if used_width < width else 0
|
157
|
-
formatted_lines = []
|
158
|
-
|
159
|
-
for line in lines:
|
160
|
-
if isinstance(line, list):
|
161
|
-
formatted_line, *counters = line
|
162
|
-
formatted_line = formatted_line.ljust(column_widths[0] + max_space)
|
163
|
-
|
164
|
-
for column, max_width in zip(counters, column_widths[1:]):
|
165
|
-
formatted_line += f"{column:>{max_width + PADDING}}"
|
166
|
-
|
167
|
-
formatted_lines.append(formatted_line)
|
168
|
-
else:
|
169
|
-
formatted_lines.append(line)
|
170
|
-
|
171
|
-
return "\n".join(formatted_lines)
|
172
|
-
|
173
|
-
|
174
|
-
PADDING = 4
|
175
|
-
HEADER = ["Links", "2xx", "4xx", "5xx", "Total"]
|
176
|
-
|
177
|
-
|
178
|
-
def _aggregate_responses(responses: ResponseCounter) -> AggregatedResponseCounter:
|
179
|
-
"""Aggregate responses by status code ranges."""
|
180
|
-
output: AggregatedResponseCounter = {
|
181
|
-
"2xx": 0,
|
182
|
-
# NOTE: 3xx responses are not counted
|
183
|
-
"4xx": 0,
|
184
|
-
"5xx": 0,
|
185
|
-
"Total": 0,
|
186
|
-
}
|
187
|
-
for status_code, count in responses.items():
|
188
|
-
if status_code is not None:
|
189
|
-
if 200 <= status_code < 300:
|
190
|
-
output["2xx"] += count
|
191
|
-
output["Total"] += count
|
192
|
-
elif 400 <= status_code < 500:
|
193
|
-
output["4xx"] += count
|
194
|
-
output["Total"] += count
|
195
|
-
elif 500 <= status_code < 600:
|
196
|
-
output["5xx"] += count
|
197
|
-
output["Total"] += count
|
198
|
-
return output
|
@@ -1,14 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from typing import TYPE_CHECKING, Callable, Dict, TypedDict, Union
|
4
|
-
|
5
|
-
if TYPE_CHECKING:
|
6
|
-
from ....stateful.state_machine import StepResult
|
7
|
-
|
8
|
-
StatusCode = str
|
9
|
-
LinkName = str
|
10
|
-
TargetName = str
|
11
|
-
SourceName = str
|
12
|
-
ResponseCounter = Dict[Union[int, None], int]
|
13
|
-
FilterFunction = Callable[["StepResult"], bool]
|
14
|
-
AggregatedResponseCounter = TypedDict("AggregatedResponseCounter", {"2xx": int, "4xx": int, "5xx": int, "Total": int})
|
@@ -1,26 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from typing import Any
|
4
|
-
|
5
|
-
from ...constants import HTTP_METHODS
|
6
|
-
|
7
|
-
|
8
|
-
def is_pattern_error(exception: TypeError) -> bool:
|
9
|
-
"""Detect whether the input exception was caused by invalid type passed to `re.search`."""
|
10
|
-
# This is intentionally simplistic and do not involve any traceback analysis
|
11
|
-
return "expected string or bytes-like object" in str(exception)
|
12
|
-
|
13
|
-
|
14
|
-
def find_numeric_http_status_codes(schema: Any) -> list[tuple[int, list[str | int]]]:
|
15
|
-
if not isinstance(schema, dict):
|
16
|
-
return []
|
17
|
-
found = []
|
18
|
-
for path, methods in schema.get("paths", {}).items():
|
19
|
-
if isinstance(methods, dict):
|
20
|
-
for method, definition in methods.items():
|
21
|
-
if method not in HTTP_METHODS or not isinstance(definition, dict):
|
22
|
-
continue
|
23
|
-
for key in definition.get("responses", {}):
|
24
|
-
if isinstance(key, int):
|
25
|
-
found.append((key, [path, method]))
|
26
|
-
return found
|
@@ -1,147 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import enum
|
4
|
-
import json
|
5
|
-
from dataclasses import dataclass, field
|
6
|
-
from typing import TYPE_CHECKING, Any, Callable, Generator
|
7
|
-
|
8
|
-
from ..constants import NOT_SET
|
9
|
-
from ..internal.result import Ok, Result
|
10
|
-
|
11
|
-
if TYPE_CHECKING:
|
12
|
-
import hypothesis
|
13
|
-
|
14
|
-
from .. import GenerationConfig
|
15
|
-
from ..exceptions import OperationSchemaError
|
16
|
-
from ..models import APIOperation, Case
|
17
|
-
from ..transports.responses import GenericResponse
|
18
|
-
from .state_machine import APIStateMachine
|
19
|
-
|
20
|
-
|
21
|
-
class UnresolvableLink(Exception):
|
22
|
-
"""Raised when a link cannot be resolved."""
|
23
|
-
|
24
|
-
|
25
|
-
@enum.unique
|
26
|
-
class Stateful(enum.Enum):
|
27
|
-
none = 1
|
28
|
-
links = 2
|
29
|
-
|
30
|
-
|
31
|
-
@dataclass
|
32
|
-
class ParsedData:
|
33
|
-
"""A structure that holds information parsed from a test outcome.
|
34
|
-
|
35
|
-
It is used later to create a new version of an API operation that will reuse this data.
|
36
|
-
"""
|
37
|
-
|
38
|
-
parameters: dict[str, Any]
|
39
|
-
body: Any = NOT_SET
|
40
|
-
|
41
|
-
def __hash__(self) -> int:
|
42
|
-
"""Custom hash simplifies deduplication of parsed data."""
|
43
|
-
value = hash(tuple(self.parameters.items())) # parameters never contain nested dicts / lists
|
44
|
-
if self.body is not NOT_SET:
|
45
|
-
if isinstance(self.body, (dict, list)):
|
46
|
-
# The simplest way to get a hash of a potentially nested structure
|
47
|
-
value ^= hash(json.dumps(self.body, sort_keys=True))
|
48
|
-
else:
|
49
|
-
# These types should be hashable
|
50
|
-
value ^= hash(self.body)
|
51
|
-
return value
|
52
|
-
|
53
|
-
|
54
|
-
@dataclass
|
55
|
-
class StatefulTest:
|
56
|
-
"""A template for a test that will be executed after another one by reusing the outcomes from it."""
|
57
|
-
|
58
|
-
name: str
|
59
|
-
|
60
|
-
def parse(self, case: Case, response: GenericResponse) -> ParsedData:
|
61
|
-
raise NotImplementedError
|
62
|
-
|
63
|
-
def is_match(self) -> bool:
|
64
|
-
raise NotImplementedError
|
65
|
-
|
66
|
-
def make_operation(self, collected: list[ParsedData]) -> APIOperation:
|
67
|
-
raise NotImplementedError
|
68
|
-
|
69
|
-
|
70
|
-
@dataclass
|
71
|
-
class StatefulData:
|
72
|
-
"""Storage for data that will be used in later tests."""
|
73
|
-
|
74
|
-
stateful_test: StatefulTest
|
75
|
-
container: list[ParsedData] = field(default_factory=list)
|
76
|
-
|
77
|
-
def make_operation(self) -> APIOperation:
|
78
|
-
return self.stateful_test.make_operation(self.container)
|
79
|
-
|
80
|
-
def store(self, case: Case, response: GenericResponse) -> None:
|
81
|
-
"""Parse and store data for a stateful test."""
|
82
|
-
try:
|
83
|
-
parsed = self.stateful_test.parse(case, response)
|
84
|
-
self.container.append(parsed)
|
85
|
-
except UnresolvableLink:
|
86
|
-
# For now, ignore if a link cannot be resolved
|
87
|
-
pass
|
88
|
-
|
89
|
-
|
90
|
-
@dataclass
|
91
|
-
class Feedback:
|
92
|
-
"""Handler for feedback from tests.
|
93
|
-
|
94
|
-
Provides a way to control runner's behavior from tests.
|
95
|
-
"""
|
96
|
-
|
97
|
-
stateful: Stateful | None
|
98
|
-
operation: APIOperation = field(repr=False)
|
99
|
-
stateful_tests: dict[str, StatefulData] = field(default_factory=dict, repr=False)
|
100
|
-
|
101
|
-
def add_test_case(self, case: Case, response: GenericResponse) -> None:
|
102
|
-
"""Store test data to reuse it in the future additional tests."""
|
103
|
-
for stateful_test in case.operation.get_stateful_tests(response, self.stateful):
|
104
|
-
data = self.stateful_tests.setdefault(stateful_test.name, StatefulData(stateful_test))
|
105
|
-
data.store(case, response)
|
106
|
-
|
107
|
-
def get_stateful_tests(
|
108
|
-
self,
|
109
|
-
test: Callable,
|
110
|
-
settings: hypothesis.settings | None,
|
111
|
-
generation_config: GenerationConfig | None,
|
112
|
-
seed: int | None,
|
113
|
-
as_strategy_kwargs: dict[str, Any] | Callable[[APIOperation], dict[str, Any]] | None,
|
114
|
-
) -> Generator[Result[tuple[APIOperation, Callable], OperationSchemaError], None, None]:
|
115
|
-
"""Generate additional tests that use data from the previous ones."""
|
116
|
-
from .._hypothesis import create_test
|
117
|
-
|
118
|
-
for data in self.stateful_tests.values():
|
119
|
-
if data.stateful_test.is_match():
|
120
|
-
operation = data.make_operation()
|
121
|
-
_as_strategy_kwargs: dict[str, Any] | None
|
122
|
-
if callable(as_strategy_kwargs):
|
123
|
-
_as_strategy_kwargs = as_strategy_kwargs(operation)
|
124
|
-
else:
|
125
|
-
_as_strategy_kwargs = as_strategy_kwargs
|
126
|
-
test_function = create_test(
|
127
|
-
operation=operation,
|
128
|
-
test=test,
|
129
|
-
settings=settings,
|
130
|
-
seed=seed,
|
131
|
-
data_generation_methods=operation.schema.data_generation_methods,
|
132
|
-
generation_config=generation_config,
|
133
|
-
as_strategy_kwargs=_as_strategy_kwargs,
|
134
|
-
)
|
135
|
-
yield Ok((operation, test_function))
|
136
|
-
|
137
|
-
|
138
|
-
def run_state_machine_as_test(
|
139
|
-
state_machine_factory: type[APIStateMachine], *, settings: hypothesis.settings | None = None
|
140
|
-
) -> None:
|
141
|
-
"""Run a state machine as a test.
|
142
|
-
|
143
|
-
It automatically adds the `_min_steps` argument if ``Hypothesis`` is recent enough.
|
144
|
-
"""
|
145
|
-
from hypothesis.stateful import run_state_machine_as_test as _run_state_machine_as_test
|
146
|
-
|
147
|
-
return _run_state_machine_as_test(state_machine_factory, settings=settings, _min_steps=2)
|
schemathesis/stateful/config.py
DELETED
@@ -1,97 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from dataclasses import dataclass, field
|
4
|
-
from datetime import timedelta
|
5
|
-
from typing import TYPE_CHECKING, Any
|
6
|
-
|
7
|
-
from ..constants import DEFAULT_DEADLINE
|
8
|
-
|
9
|
-
if TYPE_CHECKING:
|
10
|
-
import hypothesis
|
11
|
-
from requests.auth import HTTPDigestAuth
|
12
|
-
|
13
|
-
from .._override import CaseOverride
|
14
|
-
from ..models import CheckFunction
|
15
|
-
from ..targets import Target
|
16
|
-
from ..transports import RequestConfig
|
17
|
-
from ..types import RawAuth
|
18
|
-
|
19
|
-
|
20
|
-
def _default_checks_factory() -> tuple[CheckFunction, ...]:
|
21
|
-
from ..checks import ALL_CHECKS
|
22
|
-
from ..specs.openapi.checks import ensure_resource_availability, use_after_free
|
23
|
-
|
24
|
-
return (*ALL_CHECKS, use_after_free, ensure_resource_availability)
|
25
|
-
|
26
|
-
|
27
|
-
def _get_default_hypothesis_settings_kwargs() -> dict[str, Any]:
|
28
|
-
import hypothesis
|
29
|
-
|
30
|
-
return {
|
31
|
-
"phases": (hypothesis.Phase.generate,),
|
32
|
-
"deadline": None,
|
33
|
-
"stateful_step_count": 6,
|
34
|
-
"suppress_health_check": list(hypothesis.HealthCheck),
|
35
|
-
}
|
36
|
-
|
37
|
-
|
38
|
-
def _default_hypothesis_settings_factory() -> hypothesis.settings:
|
39
|
-
# To avoid importing hypothesis at the module level
|
40
|
-
import hypothesis
|
41
|
-
|
42
|
-
return hypothesis.settings(**_get_default_hypothesis_settings_kwargs())
|
43
|
-
|
44
|
-
|
45
|
-
def _default_request_config_factory() -> RequestConfig:
|
46
|
-
from ..transports import RequestConfig
|
47
|
-
|
48
|
-
return RequestConfig()
|
49
|
-
|
50
|
-
|
51
|
-
@dataclass
|
52
|
-
class StatefulTestRunnerConfig:
|
53
|
-
"""Configuration for the stateful test runner."""
|
54
|
-
|
55
|
-
# Checks to run against each response
|
56
|
-
checks: tuple[CheckFunction, ...] = field(default_factory=_default_checks_factory)
|
57
|
-
# Hypothesis settings for state machine execution
|
58
|
-
hypothesis_settings: hypothesis.settings = field(default_factory=_default_hypothesis_settings_factory)
|
59
|
-
# Request-level configuration
|
60
|
-
request: RequestConfig = field(default_factory=_default_request_config_factory)
|
61
|
-
# Whether to stop the execution after the first failure
|
62
|
-
exit_first: bool = False
|
63
|
-
max_failures: int | None = None
|
64
|
-
# Custom headers sent with each request
|
65
|
-
headers: dict[str, str] = field(default_factory=dict)
|
66
|
-
auth: HTTPDigestAuth | RawAuth | None = None
|
67
|
-
seed: int | None = None
|
68
|
-
override: CaseOverride | None = None
|
69
|
-
max_response_time: int | None = None
|
70
|
-
dry_run: bool = False
|
71
|
-
targets: list[Target] = field(default_factory=list)
|
72
|
-
unique_data: bool = False
|
73
|
-
|
74
|
-
def __post_init__(self) -> None:
|
75
|
-
import hypothesis
|
76
|
-
|
77
|
-
kwargs = _get_hypothesis_settings_kwargs_override(self.hypothesis_settings)
|
78
|
-
if kwargs:
|
79
|
-
self.hypothesis_settings = hypothesis.settings(self.hypothesis_settings, **kwargs)
|
80
|
-
|
81
|
-
|
82
|
-
def _get_hypothesis_settings_kwargs_override(settings: hypothesis.settings) -> dict[str, Any]:
|
83
|
-
"""Get the settings that should be overridden to match the defaults for API state machines."""
|
84
|
-
import hypothesis
|
85
|
-
|
86
|
-
kwargs = {}
|
87
|
-
hypothesis_default = hypothesis.settings()
|
88
|
-
state_machine_default = _default_hypothesis_settings_factory()
|
89
|
-
if settings.phases == hypothesis_default.phases:
|
90
|
-
kwargs["phases"] = state_machine_default.phases
|
91
|
-
if settings.stateful_step_count == hypothesis_default.stateful_step_count:
|
92
|
-
kwargs["stateful_step_count"] = state_machine_default.stateful_step_count
|
93
|
-
if settings.deadline in (hypothesis_default.deadline, timedelta(milliseconds=DEFAULT_DEADLINE)):
|
94
|
-
kwargs["deadline"] = state_machine_default.deadline
|
95
|
-
if settings.suppress_health_check == hypothesis_default.suppress_health_check:
|
96
|
-
kwargs["suppress_health_check"] = state_machine_default.suppress_health_check
|
97
|
-
return kwargs
|
schemathesis/stateful/context.py
DELETED
@@ -1,135 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import traceback
|
4
|
-
from dataclasses import dataclass, field
|
5
|
-
from typing import TYPE_CHECKING, Tuple, Type, Union
|
6
|
-
|
7
|
-
from ..constants import NOT_SET
|
8
|
-
from ..exceptions import CheckFailed
|
9
|
-
from ..targets import TargetMetricCollector
|
10
|
-
from . import events
|
11
|
-
|
12
|
-
if TYPE_CHECKING:
|
13
|
-
from ..models import Case, Check
|
14
|
-
from ..transports.responses import GenericResponse
|
15
|
-
from ..types import NotSet
|
16
|
-
|
17
|
-
FailureKey = Union[Type[CheckFailed], Tuple[str, int]]
|
18
|
-
|
19
|
-
|
20
|
-
def _failure_cache_key(exc: CheckFailed | AssertionError) -> FailureKey:
|
21
|
-
"""Create a key to identify unique failures."""
|
22
|
-
from hypothesis.internal.escalation import get_trimmed_traceback
|
23
|
-
|
24
|
-
# For CheckFailed, we already have all distinctive information about the failure, which is contained
|
25
|
-
# in the exception type itself.
|
26
|
-
if isinstance(exc, CheckFailed):
|
27
|
-
return exc.__class__
|
28
|
-
|
29
|
-
# Assertion come from the user's code and we may try to group them by location
|
30
|
-
tb = get_trimmed_traceback(exc)
|
31
|
-
filename, lineno, *_ = traceback.extract_tb(tb)[-1]
|
32
|
-
return (filename, lineno)
|
33
|
-
|
34
|
-
|
35
|
-
@dataclass
|
36
|
-
class RunnerContext:
|
37
|
-
"""Mutable context for state machine execution."""
|
38
|
-
|
39
|
-
# All seen failure keys, both grouped and individual ones
|
40
|
-
seen_in_run: set[FailureKey] = field(default_factory=set)
|
41
|
-
# Failures keys seen in the current suite
|
42
|
-
seen_in_suite: set[FailureKey] = field(default_factory=set)
|
43
|
-
# Unique failures collected in the current suite
|
44
|
-
failures_for_suite: list[Check] = field(default_factory=list)
|
45
|
-
# All checks executed in the current run
|
46
|
-
checks_for_step: list[Check] = field(default_factory=list)
|
47
|
-
# Status of the current step
|
48
|
-
current_step_status: events.StepStatus | None = None
|
49
|
-
# The currently processed response
|
50
|
-
current_response: GenericResponse | None = None
|
51
|
-
# Total number of failures
|
52
|
-
failures_count: int = 0
|
53
|
-
# The total number of completed test scenario
|
54
|
-
completed_scenarios: int = 0
|
55
|
-
# Metrics collector for targeted testing
|
56
|
-
metric_collector: TargetMetricCollector = field(default_factory=lambda: TargetMetricCollector(targets=[]))
|
57
|
-
step_outcomes: dict[int, BaseException | None] = field(default_factory=dict)
|
58
|
-
|
59
|
-
@property
|
60
|
-
def current_scenario_status(self) -> events.ScenarioStatus:
|
61
|
-
if self.current_step_status == events.StepStatus.SUCCESS:
|
62
|
-
return events.ScenarioStatus.SUCCESS
|
63
|
-
if self.current_step_status == events.StepStatus.FAILURE:
|
64
|
-
return events.ScenarioStatus.FAILURE
|
65
|
-
if self.current_step_status == events.StepStatus.ERROR:
|
66
|
-
return events.ScenarioStatus.ERROR
|
67
|
-
if self.current_step_status == events.StepStatus.INTERRUPTED:
|
68
|
-
return events.ScenarioStatus.INTERRUPTED
|
69
|
-
return events.ScenarioStatus.REJECTED
|
70
|
-
|
71
|
-
def reset_scenario(self) -> None:
|
72
|
-
self.completed_scenarios += 1
|
73
|
-
self.current_step_status = None
|
74
|
-
self.current_response = None
|
75
|
-
self.step_outcomes.clear()
|
76
|
-
|
77
|
-
def reset_step(self) -> None:
|
78
|
-
self.checks_for_step = []
|
79
|
-
|
80
|
-
def step_succeeded(self) -> None:
|
81
|
-
self.current_step_status = events.StepStatus.SUCCESS
|
82
|
-
|
83
|
-
def step_failed(self) -> None:
|
84
|
-
self.current_step_status = events.StepStatus.FAILURE
|
85
|
-
|
86
|
-
def step_errored(self) -> None:
|
87
|
-
self.current_step_status = events.StepStatus.ERROR
|
88
|
-
|
89
|
-
def step_interrupted(self) -> None:
|
90
|
-
self.current_step_status = events.StepStatus.INTERRUPTED
|
91
|
-
|
92
|
-
def mark_as_seen_in_run(self, exc: CheckFailed) -> None:
|
93
|
-
key = _failure_cache_key(exc)
|
94
|
-
self.seen_in_run.add(key)
|
95
|
-
causes = exc.causes or ()
|
96
|
-
for cause in causes:
|
97
|
-
key = _failure_cache_key(cause)
|
98
|
-
self.seen_in_run.add(key)
|
99
|
-
|
100
|
-
def mark_as_seen_in_suite(self, exc: CheckFailed | AssertionError) -> None:
|
101
|
-
key = _failure_cache_key(exc)
|
102
|
-
self.seen_in_suite.add(key)
|
103
|
-
|
104
|
-
def mark_current_suite_as_seen_in_run(self) -> None:
|
105
|
-
self.seen_in_run.update(self.seen_in_suite)
|
106
|
-
|
107
|
-
def is_seen_in_run(self, exc: CheckFailed | AssertionError) -> bool:
|
108
|
-
key = _failure_cache_key(exc)
|
109
|
-
return key in self.seen_in_run
|
110
|
-
|
111
|
-
def is_seen_in_suite(self, exc: CheckFailed | AssertionError) -> bool:
|
112
|
-
key = _failure_cache_key(exc)
|
113
|
-
return key in self.seen_in_suite
|
114
|
-
|
115
|
-
def add_failed_check(self, check: Check) -> None:
|
116
|
-
self.failures_for_suite.append(check)
|
117
|
-
self.failures_count += 1
|
118
|
-
|
119
|
-
def collect_metric(self, case: Case, response: GenericResponse) -> None:
|
120
|
-
self.metric_collector.store(case, response)
|
121
|
-
|
122
|
-
def maximize_metrics(self) -> None:
|
123
|
-
self.metric_collector.maximize()
|
124
|
-
|
125
|
-
def reset(self) -> None:
|
126
|
-
self.failures_for_suite = []
|
127
|
-
self.seen_in_suite.clear()
|
128
|
-
self.reset_scenario()
|
129
|
-
self.metric_collector.reset()
|
130
|
-
|
131
|
-
def store_step_outcome(self, case: Case, outcome: BaseException | None) -> None:
|
132
|
-
self.step_outcomes[hash(case)] = outcome
|
133
|
-
|
134
|
-
def get_step_outcome(self, case: Case) -> BaseException | None | NotSet:
|
135
|
-
return self.step_outcomes.get(hash(case), NOT_SET)
|