schemathesis 4.0.0a1__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/checks.py +6 -4
- schemathesis/cli/commands/run/__init__.py +4 -4
- schemathesis/cli/commands/run/events.py +4 -9
- schemathesis/cli/commands/run/executor.py +6 -3
- schemathesis/cli/commands/run/filters.py +27 -19
- schemathesis/cli/commands/run/handlers/base.py +1 -1
- schemathesis/cli/commands/run/handlers/cassettes.py +1 -3
- schemathesis/cli/commands/run/handlers/output.py +765 -143
- schemathesis/cli/commands/run/validation.py +1 -1
- schemathesis/cli/ext/options.py +4 -1
- schemathesis/core/failures.py +54 -24
- schemathesis/engine/core.py +1 -1
- schemathesis/engine/events.py +3 -97
- schemathesis/engine/phases/stateful/__init__.py +1 -0
- schemathesis/engine/phases/stateful/_executor.py +19 -44
- schemathesis/engine/phases/unit/__init__.py +1 -0
- schemathesis/engine/phases/unit/_executor.py +2 -1
- schemathesis/engine/phases/unit/_pool.py +1 -1
- schemathesis/engine/recorder.py +8 -3
- schemathesis/generation/stateful/state_machine.py +53 -36
- schemathesis/graphql/checks.py +3 -9
- schemathesis/openapi/checks.py +8 -33
- schemathesis/schemas.py +34 -14
- schemathesis/specs/graphql/schemas.py +16 -15
- schemathesis/specs/openapi/expressions/__init__.py +11 -15
- schemathesis/specs/openapi/expressions/nodes.py +20 -20
- schemathesis/specs/openapi/links.py +126 -119
- schemathesis/specs/openapi/schemas.py +18 -22
- schemathesis/specs/openapi/stateful/__init__.py +77 -55
- {schemathesis-4.0.0a1.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +1 -1
- {schemathesis-4.0.0a1.dist-info → schemathesis-4.0.0a2.dist-info}/RECORD +34 -35
- schemathesis/specs/openapi/expressions/context.py +0 -14
- {schemathesis-4.0.0a1.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.0a1.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.0.0a1.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
schemathesis/checks.py
CHANGED
@@ -4,6 +4,7 @@ import json
|
|
4
4
|
from typing import TYPE_CHECKING, Any, Callable, Iterable, Iterator, Optional
|
5
5
|
|
6
6
|
from schemathesis.core.failures import (
|
7
|
+
CustomFailure,
|
7
8
|
Failure,
|
8
9
|
FailureGroup,
|
9
10
|
MalformedJson,
|
@@ -72,7 +73,7 @@ class CheckContext:
|
|
72
73
|
|
73
74
|
def record_case(self, *, parent_id: str, case: Case) -> None:
|
74
75
|
if self.recorder is not None:
|
75
|
-
self.recorder.record_case(parent_id=parent_id, case=case)
|
76
|
+
self.recorder.record_case(parent_id=parent_id, transition=None, case=case)
|
76
77
|
|
77
78
|
def record_response(self, *, case_id: str, response: Response) -> None:
|
78
79
|
if self.recorder is not None:
|
@@ -135,10 +136,11 @@ def run_checks(
|
|
135
136
|
except Failure as failure:
|
136
137
|
on_failure(name, collected, failure.with_traceback(None))
|
137
138
|
except AssertionError as exc:
|
138
|
-
custom_failure =
|
139
|
-
name=name,
|
139
|
+
custom_failure = CustomFailure(
|
140
140
|
operation=case.operation.label,
|
141
|
-
|
141
|
+
title=f"Custom check failed: `{name}`",
|
142
|
+
message=str(exc),
|
143
|
+
exception=exc,
|
142
144
|
)
|
143
145
|
on_failure(name, collected, custom_failure)
|
144
146
|
except FailureGroup as group:
|
@@ -91,7 +91,7 @@ DEFAULT_PHASES = ("unit", "stateful")
|
|
91
91
|
help="Comma-separated list of checks to run against API responses",
|
92
92
|
type=RegistryChoice(CHECKS, with_all=True),
|
93
93
|
default=("not_a_server_error",),
|
94
|
-
callback=validation.
|
94
|
+
callback=validation.reduce_list,
|
95
95
|
show_default=True,
|
96
96
|
metavar="",
|
97
97
|
)
|
@@ -102,7 +102,7 @@ DEFAULT_PHASES = ("unit", "stateful")
|
|
102
102
|
help="Comma-separated list of checks to skip during testing",
|
103
103
|
type=RegistryChoice(CHECKS, with_all=True),
|
104
104
|
default=(),
|
105
|
-
callback=validation.
|
105
|
+
callback=validation.reduce_list,
|
106
106
|
show_default=True,
|
107
107
|
metavar="",
|
108
108
|
)
|
@@ -356,7 +356,7 @@ DEFAULT_PHASES = ("unit", "stateful")
|
|
356
356
|
help="Guide input generation to values more likely to expose bugs via targeted property-based testing",
|
357
357
|
type=RegistryChoice(TARGETS),
|
358
358
|
default=None,
|
359
|
-
callback=validation.
|
359
|
+
callback=validation.reduce_list,
|
360
360
|
show_default=True,
|
361
361
|
metavar="TARGET",
|
362
362
|
)
|
@@ -588,7 +588,7 @@ def run(
|
|
588
588
|
max_response_time=max_response_time,
|
589
589
|
).into()
|
590
590
|
|
591
|
-
if exit_first:
|
591
|
+
if exit_first and max_failures is None:
|
592
592
|
max_failures = 1
|
593
593
|
|
594
594
|
cassette_config = None
|
@@ -3,7 +3,7 @@ import uuid
|
|
3
3
|
|
4
4
|
from schemathesis.core import Specification
|
5
5
|
from schemathesis.engine import events
|
6
|
-
from schemathesis.schemas import
|
6
|
+
from schemathesis.schemas import ApiStatistic
|
7
7
|
|
8
8
|
|
9
9
|
class LoadingStarted(events.EngineEvent):
|
@@ -16,15 +16,10 @@ class LoadingStarted(events.EngineEvent):
|
|
16
16
|
|
17
17
|
|
18
18
|
class LoadingFinished(events.EngineEvent):
|
19
|
-
__slots__ = ("id", "timestamp", "location", "duration", "base_url", "specification", "
|
19
|
+
__slots__ = ("id", "timestamp", "location", "duration", "base_url", "specification", "statistic")
|
20
20
|
|
21
21
|
def __init__(
|
22
|
-
self,
|
23
|
-
location: str,
|
24
|
-
start_time: float,
|
25
|
-
base_url: str,
|
26
|
-
specification: Specification,
|
27
|
-
operations_count: ApiOperationsCount,
|
22
|
+
self, location: str, start_time: float, base_url: str, specification: Specification, statistic: ApiStatistic
|
28
23
|
) -> None:
|
29
24
|
self.id = uuid.uuid4()
|
30
25
|
self.timestamp = time.time()
|
@@ -32,4 +27,4 @@ class LoadingFinished(events.EngineEvent):
|
|
32
27
|
self.duration = self.timestamp - start_time
|
33
28
|
self.base_url = base_url
|
34
29
|
self.specification = specification
|
35
|
-
self.
|
30
|
+
self.statistic = statistic
|
@@ -19,7 +19,7 @@ from schemathesis.core.errors import LoaderError
|
|
19
19
|
from schemathesis.core.output import OutputConfig
|
20
20
|
from schemathesis.engine import from_schema
|
21
21
|
from schemathesis.engine.config import EngineConfig
|
22
|
-
from schemathesis.engine.events import EventGenerator, FatalError
|
22
|
+
from schemathesis.engine.events import EventGenerator, FatalError, Interrupted
|
23
23
|
from schemathesis.filters import FilterSet
|
24
24
|
|
25
25
|
CUSTOM_HANDLERS: list[type[EventHandler]] = []
|
@@ -70,6 +70,9 @@ def into_event_stream(config: RunConfig) -> EventGenerator:
|
|
70
70
|
try:
|
71
71
|
schema = load_schema(loader_config)
|
72
72
|
schema.filter_set = config.filter_set
|
73
|
+
except KeyboardInterrupt:
|
74
|
+
yield Interrupted(phase=None)
|
75
|
+
return
|
73
76
|
except LoaderError as exc:
|
74
77
|
yield FatalError(exception=exc)
|
75
78
|
return
|
@@ -79,7 +82,7 @@ def into_event_stream(config: RunConfig) -> EventGenerator:
|
|
79
82
|
start_time=loading_started.timestamp,
|
80
83
|
base_url=schema.get_base_url(),
|
81
84
|
specification=schema.specification,
|
82
|
-
|
85
|
+
statistic=schema.statistic,
|
83
86
|
)
|
84
87
|
|
85
88
|
try:
|
@@ -112,7 +115,7 @@ def _execute(event_stream: EventGenerator, config: RunConfig) -> None:
|
|
112
115
|
|
113
116
|
def shutdown() -> None:
|
114
117
|
for _handler in handlers:
|
115
|
-
_handler.shutdown()
|
118
|
+
_handler.shutdown(ctx)
|
116
119
|
|
117
120
|
for handler in handlers:
|
118
121
|
handler.start(ctx)
|
@@ -1,11 +1,12 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
from dataclasses import dataclass
|
4
|
-
from typing import Callable, Literal, Sequence
|
4
|
+
from typing import Any, Callable, Literal, Sequence
|
5
5
|
|
6
6
|
import click
|
7
7
|
|
8
8
|
from schemathesis.cli.ext.groups import grouped_option
|
9
|
+
from schemathesis.core.errors import IncorrectUsage
|
9
10
|
from schemathesis.filters import FilterSet, expression_to_filter_function, is_deprecated
|
10
11
|
|
11
12
|
|
@@ -148,29 +149,24 @@ class FilterArguments:
|
|
148
149
|
if exclude_by_function:
|
149
150
|
filter_set.exclude(exclude_by_function)
|
150
151
|
for name_ in self.exclude_name:
|
151
|
-
filter_set
|
152
|
+
apply_exclude_filter(filter_set, "name", name=name_)
|
152
153
|
for method in self.exclude_method:
|
153
|
-
filter_set
|
154
|
+
apply_exclude_filter(filter_set, "method", method=method)
|
154
155
|
for path in self.exclude_path:
|
155
|
-
filter_set
|
156
|
+
apply_exclude_filter(filter_set, "path", path=path)
|
156
157
|
for tag in self.exclude_tag:
|
157
|
-
filter_set
|
158
|
+
apply_exclude_filter(filter_set, "tag", tag=tag)
|
158
159
|
for operation_id in self.exclude_operation_id:
|
159
|
-
filter_set
|
160
|
-
|
161
|
-
self.exclude_name_regex
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
160
|
+
apply_exclude_filter(filter_set, "operation-id", operation_id=operation_id)
|
161
|
+
for key, value, name in (
|
162
|
+
("name_regex", self.exclude_name_regex, "name-regex"),
|
163
|
+
("method_regex", self.exclude_method_regex, "method-regex"),
|
164
|
+
("path_regex", self.exclude_path_regex, "path-regex"),
|
165
|
+
("tag_regex", self.exclude_tag_regex, "tag-regex"),
|
166
|
+
("operation_id_regex", self.exclude_operation_id_regex, "operation-id-regex"),
|
166
167
|
):
|
167
|
-
|
168
|
-
|
169
|
-
method_regex=self.exclude_method_regex,
|
170
|
-
path_regex=self.exclude_path_regex,
|
171
|
-
tag_regex=self.exclude_tag_regex,
|
172
|
-
operation_id_regex=self.exclude_operation_id_regex,
|
173
|
-
)
|
168
|
+
if value:
|
169
|
+
apply_exclude_filter(filter_set, name, **{key: value})
|
174
170
|
|
175
171
|
# Exclude deprecated operations
|
176
172
|
if self.exclude_deprecated:
|
@@ -179,6 +175,18 @@ class FilterArguments:
|
|
179
175
|
return filter_set
|
180
176
|
|
181
177
|
|
178
|
+
def apply_exclude_filter(filter_set: FilterSet, option_name: str, **kwargs: Any) -> None:
|
179
|
+
"""Apply an exclude filter with proper error handling."""
|
180
|
+
try:
|
181
|
+
filter_set.exclude(**kwargs)
|
182
|
+
except IncorrectUsage as e:
|
183
|
+
if str(e) == "Filter already exists":
|
184
|
+
raise click.UsageError(
|
185
|
+
f"Filter for {option_name} already exists. You can't simultaneously include and exclude the same thing."
|
186
|
+
) from None
|
187
|
+
raise click.UsageError(str(e)) from None
|
188
|
+
|
189
|
+
|
182
190
|
def validate_unique_filter(values: Sequence[str], arg_name: str) -> None:
|
183
191
|
if len(values) != len(set(values)):
|
184
192
|
duplicates = ",".join(sorted({value for value in values if values.count(value) > 1}))
|
@@ -77,10 +77,8 @@ class CassetteWriter(EventHandler):
|
|
77
77
|
def handle_event(self, ctx: ExecutionContext, event: events.EngineEvent) -> None:
|
78
78
|
if isinstance(event, events.ScenarioFinished):
|
79
79
|
self.queue.put(Process(recorder=event.recorder))
|
80
|
-
elif isinstance(event, events.EngineFinished):
|
81
|
-
self.shutdown()
|
82
80
|
|
83
|
-
def shutdown(self) -> None:
|
81
|
+
def shutdown(self, ctx: ExecutionContext) -> None:
|
84
82
|
self.queue.put(Finalize())
|
85
83
|
self._stop_worker()
|
86
84
|
|