schemathesis 3.29.2__py3-none-any.whl → 3.30.1__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 +3 -3
- schemathesis/_compat.py +2 -2
- schemathesis/_dependency_versions.py +1 -3
- schemathesis/_hypothesis.py +6 -0
- schemathesis/_lazy_import.py +1 -0
- schemathesis/_override.py +1 -0
- schemathesis/_rate_limiter.py +2 -1
- schemathesis/_xml.py +1 -0
- schemathesis/auths.py +4 -2
- schemathesis/checks.py +8 -5
- schemathesis/cli/__init__.py +28 -1
- schemathesis/cli/callbacks.py +3 -4
- schemathesis/cli/cassettes.py +6 -4
- schemathesis/cli/constants.py +2 -0
- schemathesis/cli/context.py +5 -0
- schemathesis/cli/debug.py +2 -1
- schemathesis/cli/handlers.py +1 -1
- schemathesis/cli/junitxml.py +5 -4
- schemathesis/cli/options.py +1 -0
- schemathesis/cli/output/default.py +56 -24
- schemathesis/cli/output/short.py +21 -10
- schemathesis/cli/sanitization.py +1 -0
- schemathesis/code_samples.py +1 -0
- schemathesis/constants.py +1 -0
- schemathesis/contrib/openapi/__init__.py +1 -1
- schemathesis/contrib/openapi/fill_missing_examples.py +2 -0
- schemathesis/contrib/openapi/formats/uuid.py +2 -1
- schemathesis/contrib/unique_data.py +2 -1
- schemathesis/exceptions.py +42 -61
- schemathesis/experimental/__init__.py +14 -0
- schemathesis/extra/_aiohttp.py +1 -0
- schemathesis/extra/_server.py +1 -0
- schemathesis/extra/pytest_plugin.py +13 -24
- schemathesis/failures.py +42 -8
- schemathesis/filters.py +2 -1
- schemathesis/fixups/__init__.py +1 -0
- schemathesis/fixups/fast_api.py +2 -2
- schemathesis/fixups/utf8_bom.py +1 -2
- schemathesis/generation/__init__.py +2 -1
- schemathesis/hooks.py +3 -1
- schemathesis/internal/copy.py +19 -3
- schemathesis/internal/deprecation.py +1 -1
- schemathesis/internal/jsonschema.py +2 -1
- schemathesis/internal/output.py +68 -0
- schemathesis/internal/result.py +1 -1
- schemathesis/internal/transformation.py +1 -0
- schemathesis/lazy.py +11 -2
- schemathesis/loaders.py +4 -2
- schemathesis/models.py +22 -7
- schemathesis/parameters.py +1 -0
- schemathesis/runner/__init__.py +1 -1
- schemathesis/runner/events.py +22 -4
- schemathesis/runner/impl/core.py +69 -33
- schemathesis/runner/impl/solo.py +2 -1
- schemathesis/runner/impl/threadpool.py +4 -0
- schemathesis/runner/probes.py +1 -1
- schemathesis/runner/serialization.py +1 -1
- schemathesis/sanitization.py +2 -0
- schemathesis/schemas.py +7 -4
- schemathesis/service/ci.py +1 -0
- schemathesis/service/client.py +7 -7
- schemathesis/service/events.py +2 -1
- schemathesis/service/extensions.py +5 -5
- schemathesis/service/hosts.py +1 -0
- schemathesis/service/metadata.py +2 -1
- schemathesis/service/models.py +2 -1
- schemathesis/service/report.py +3 -3
- schemathesis/service/serialization.py +62 -23
- schemathesis/service/usage.py +1 -0
- schemathesis/specs/graphql/_cache.py +1 -1
- schemathesis/specs/graphql/loaders.py +17 -1
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +2 -2
- schemathesis/specs/graphql/schemas.py +7 -7
- schemathesis/specs/graphql/validation.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +17 -11
- schemathesis/specs/openapi/checks.py +102 -9
- schemathesis/specs/openapi/converter.py +2 -1
- schemathesis/specs/openapi/definitions.py +2 -1
- schemathesis/specs/openapi/examples.py +7 -9
- schemathesis/specs/openapi/expressions/__init__.py +29 -2
- schemathesis/specs/openapi/expressions/context.py +1 -1
- schemathesis/specs/openapi/expressions/extractors.py +23 -0
- schemathesis/specs/openapi/expressions/lexer.py +19 -18
- schemathesis/specs/openapi/expressions/nodes.py +24 -4
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/filters.py +1 -0
- schemathesis/specs/openapi/links.py +35 -7
- schemathesis/specs/openapi/loaders.py +31 -11
- schemathesis/specs/openapi/negative/__init__.py +2 -1
- schemathesis/specs/openapi/negative/mutations.py +1 -0
- schemathesis/specs/openapi/parameters.py +1 -0
- schemathesis/specs/openapi/schemas.py +28 -39
- schemathesis/specs/openapi/security.py +1 -0
- schemathesis/specs/openapi/serialization.py +1 -0
- schemathesis/specs/openapi/stateful/__init__.py +159 -70
- schemathesis/specs/openapi/stateful/statistic.py +198 -0
- schemathesis/specs/openapi/stateful/types.py +13 -0
- schemathesis/specs/openapi/utils.py +1 -0
- schemathesis/specs/openapi/validation.py +1 -0
- schemathesis/stateful/__init__.py +4 -2
- schemathesis/stateful/config.py +66 -0
- schemathesis/stateful/context.py +103 -0
- schemathesis/stateful/events.py +215 -0
- schemathesis/stateful/runner.py +238 -0
- schemathesis/stateful/sink.py +68 -0
- schemathesis/stateful/state_machine.py +39 -22
- schemathesis/stateful/statistic.py +20 -0
- schemathesis/stateful/validation.py +66 -0
- schemathesis/targets.py +1 -0
- schemathesis/throttling.py +23 -3
- schemathesis/transports/__init__.py +28 -10
- schemathesis/transports/auth.py +1 -0
- schemathesis/transports/content_types.py +1 -1
- schemathesis/transports/headers.py +2 -1
- schemathesis/transports/responses.py +6 -4
- schemathesis/types.py +1 -0
- schemathesis/utils.py +1 -0
- {schemathesis-3.29.2.dist-info → schemathesis-3.30.1.dist-info}/METADATA +3 -3
- schemathesis-3.30.1.dist-info/RECORD +151 -0
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis-3.29.2.dist-info/RECORD +0 -141
- {schemathesis-3.29.2.dist-info → schemathesis-3.30.1.dist-info}/WHEEL +0 -0
- {schemathesis-3.29.2.dist-info → schemathesis-3.30.1.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.29.2.dist-info → schemathesis-3.30.1.dist-info}/licenses/LICENSE +0 -0
schemathesis/cli/output/short.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import click
|
|
2
2
|
|
|
3
3
|
from ...runner import events
|
|
4
|
+
from ...stateful import events as stateful_events
|
|
4
5
|
from ..context import ExecutionContext
|
|
5
6
|
from ..handlers import EventHandler
|
|
6
7
|
from . import default
|
|
@@ -15,7 +16,13 @@ def handle_after_execution(context: ExecutionContext, event: events.AfterExecuti
|
|
|
15
16
|
context.operations_processed += 1
|
|
16
17
|
context.results.append(event.result)
|
|
17
18
|
context.hypothesis_output.extend(event.hypothesis_output)
|
|
18
|
-
default.display_execution_result(context, event)
|
|
19
|
+
default.display_execution_result(context, event.status.value)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def handle_stateful_event(context: ExecutionContext, event: events.StatefulEvent) -> None:
|
|
23
|
+
if isinstance(event.data, stateful_events.RunStarted):
|
|
24
|
+
click.echo()
|
|
25
|
+
default.handle_stateful_event(context, event)
|
|
19
26
|
|
|
20
27
|
|
|
21
28
|
class ShortOutputStyleHandler(EventHandler):
|
|
@@ -26,23 +33,27 @@ class ShortOutputStyleHandler(EventHandler):
|
|
|
26
33
|
"""
|
|
27
34
|
if isinstance(event, events.Initialized):
|
|
28
35
|
default.handle_initialized(context, event)
|
|
29
|
-
|
|
36
|
+
elif isinstance(event, events.BeforeProbing):
|
|
30
37
|
default.handle_before_probing(context, event)
|
|
31
|
-
|
|
38
|
+
elif isinstance(event, events.AfterProbing):
|
|
32
39
|
default.handle_after_probing(context, event)
|
|
33
|
-
|
|
40
|
+
elif isinstance(event, events.BeforeAnalysis):
|
|
34
41
|
default.handle_before_analysis(context, event)
|
|
35
|
-
|
|
42
|
+
elif isinstance(event, events.AfterAnalysis):
|
|
36
43
|
default.handle_after_analysis(context, event)
|
|
37
|
-
|
|
44
|
+
elif isinstance(event, events.BeforeExecution):
|
|
38
45
|
handle_before_execution(context, event)
|
|
39
|
-
|
|
46
|
+
elif isinstance(event, events.AfterExecution):
|
|
40
47
|
handle_after_execution(context, event)
|
|
41
|
-
|
|
48
|
+
elif isinstance(event, events.Finished):
|
|
42
49
|
if context.operations_count == context.operations_processed:
|
|
43
50
|
click.echo()
|
|
44
51
|
default.handle_finished(context, event)
|
|
45
|
-
|
|
52
|
+
elif isinstance(event, events.Interrupted):
|
|
46
53
|
default.handle_interrupted(context, event)
|
|
47
|
-
|
|
54
|
+
elif isinstance(event, events.InternalError):
|
|
48
55
|
default.handle_internal_error(context, event)
|
|
56
|
+
elif isinstance(event, events.StatefulEvent):
|
|
57
|
+
handle_stateful_event(context, event)
|
|
58
|
+
elif isinstance(event, events.AfterStatefulExecution):
|
|
59
|
+
default.handle_after_stateful_execution(context, event)
|
schemathesis/cli/sanitization.py
CHANGED
schemathesis/code_samples.py
CHANGED
schemathesis/constants.py
CHANGED
|
@@ -6,9 +6,10 @@ from typing import TYPE_CHECKING
|
|
|
6
6
|
from ..hooks import HookContext, register, unregister
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
|
-
from ..models import Case
|
|
10
9
|
from hypothesis import strategies as st
|
|
11
10
|
|
|
11
|
+
from ..models import Case
|
|
12
|
+
|
|
12
13
|
|
|
13
14
|
def install() -> None:
|
|
14
15
|
warnings.warn(
|
schemathesis/exceptions.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import enum
|
|
3
|
-
import json
|
|
4
4
|
import re
|
|
5
5
|
import traceback
|
|
6
6
|
from dataclasses import dataclass, field
|
|
@@ -11,14 +11,17 @@ from typing import TYPE_CHECKING, Any, Callable, Generator, NoReturn
|
|
|
11
11
|
|
|
12
12
|
from .constants import SERIALIZERS_SUGGESTION_MESSAGE
|
|
13
13
|
from .failures import FailureContext
|
|
14
|
+
from .internal.output import truncate_json
|
|
14
15
|
|
|
15
16
|
if TYPE_CHECKING:
|
|
16
17
|
import hypothesis.errors
|
|
17
|
-
from jsonschema import RefResolutionError, ValidationError, SchemaError as JsonSchemaError
|
|
18
|
-
from .transports.responses import GenericResponse
|
|
19
18
|
from graphql.error import GraphQLFormattedError
|
|
19
|
+
from jsonschema import RefResolutionError, ValidationError
|
|
20
|
+
from jsonschema import SchemaError as JsonSchemaError
|
|
20
21
|
from requests import RequestException
|
|
21
22
|
|
|
23
|
+
from .transports.responses import GenericResponse
|
|
24
|
+
|
|
22
25
|
|
|
23
26
|
class CheckFailed(AssertionError):
|
|
24
27
|
"""Custom error type to distinguish from arbitrary AssertionError that may happen in the dependent libraries."""
|
|
@@ -97,52 +100,60 @@ def get_grouped_exception(prefix: str, *exceptions: AssertionError) -> type[Chec
|
|
|
97
100
|
return _get_hashed_exception("GroupedException", f"{prefix}{message}")
|
|
98
101
|
|
|
99
102
|
|
|
100
|
-
def get_server_error(status_code: int) -> type[CheckFailed]:
|
|
103
|
+
def get_server_error(prefix: str, status_code: int) -> type[CheckFailed]:
|
|
101
104
|
"""Return new exception for the Internal Server Error cases."""
|
|
102
|
-
name = f"ServerError{status_code}"
|
|
105
|
+
name = f"ServerError{prefix}{status_code}"
|
|
103
106
|
return get_exception(name)
|
|
104
107
|
|
|
105
108
|
|
|
106
|
-
def get_status_code_error(status_code: int) -> type[CheckFailed]:
|
|
109
|
+
def get_status_code_error(prefix: str, status_code: int) -> type[CheckFailed]:
|
|
107
110
|
"""Return new exception for an unexpected status code."""
|
|
108
|
-
name = f"StatusCodeError{status_code}"
|
|
111
|
+
name = f"StatusCodeError{prefix}{status_code}"
|
|
109
112
|
return get_exception(name)
|
|
110
113
|
|
|
111
114
|
|
|
112
|
-
def get_response_type_error(expected: str, received: str) -> type[CheckFailed]:
|
|
115
|
+
def get_response_type_error(prefix: str, expected: str, received: str) -> type[CheckFailed]:
|
|
113
116
|
"""Return new exception for an unexpected response type."""
|
|
114
|
-
name = f"SchemaValidationError{expected}_{received}"
|
|
117
|
+
name = f"SchemaValidationError{prefix}{expected}_{received}"
|
|
115
118
|
return get_exception(name)
|
|
116
119
|
|
|
117
120
|
|
|
118
|
-
def get_malformed_media_type_error(media_type: str) -> type[CheckFailed]:
|
|
119
|
-
name = f"MalformedMediaType{media_type}"
|
|
121
|
+
def get_malformed_media_type_error(prefix: str, media_type: str) -> type[CheckFailed]:
|
|
122
|
+
name = f"MalformedMediaType{prefix}{media_type}"
|
|
120
123
|
return get_exception(name)
|
|
121
124
|
|
|
122
125
|
|
|
123
|
-
def get_missing_content_type_error() -> type[CheckFailed]:
|
|
126
|
+
def get_missing_content_type_error(prefix: str) -> type[CheckFailed]:
|
|
124
127
|
"""Return new exception for a missing Content-Type header."""
|
|
125
|
-
return get_exception("MissingContentTypeError")
|
|
128
|
+
return get_exception(f"MissingContentTypeError{prefix}")
|
|
126
129
|
|
|
127
130
|
|
|
128
|
-
def get_schema_validation_error(exception: ValidationError) -> type[CheckFailed]:
|
|
131
|
+
def get_schema_validation_error(prefix: str, exception: ValidationError) -> type[CheckFailed]:
|
|
129
132
|
"""Return new exception for schema validation error."""
|
|
130
|
-
return _get_hashed_exception("SchemaValidationError", str(exception))
|
|
133
|
+
return _get_hashed_exception(f"SchemaValidationError{prefix}", str(exception))
|
|
131
134
|
|
|
132
135
|
|
|
133
|
-
def get_response_parsing_error(exception: JSONDecodeError) -> type[CheckFailed]:
|
|
136
|
+
def get_response_parsing_error(prefix: str, exception: JSONDecodeError) -> type[CheckFailed]:
|
|
134
137
|
"""Return new exception for response parsing error."""
|
|
135
|
-
return _get_hashed_exception("ResponseParsingError", str(exception))
|
|
138
|
+
return _get_hashed_exception(f"ResponseParsingError{prefix}", str(exception))
|
|
136
139
|
|
|
137
140
|
|
|
138
|
-
def get_headers_error(message: str) -> type[CheckFailed]:
|
|
141
|
+
def get_headers_error(prefix: str, message: str) -> type[CheckFailed]:
|
|
139
142
|
"""Return new exception for missing headers."""
|
|
140
|
-
return _get_hashed_exception("MissingHeadersError", message)
|
|
143
|
+
return _get_hashed_exception(f"MissingHeadersError{prefix}", message)
|
|
144
|
+
|
|
141
145
|
|
|
146
|
+
def get_negative_rejection_error(prefix: str, status: int) -> type[CheckFailed]:
|
|
147
|
+
return _get_hashed_exception(f"AcceptedNegativeDataError{prefix}", str(status))
|
|
142
148
|
|
|
143
|
-
|
|
149
|
+
|
|
150
|
+
def get_use_after_free_error(free: str) -> type[CheckFailed]:
|
|
151
|
+
return _get_hashed_exception("UseAfterFreeError", free)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def get_timeout_error(prefix: str, deadline: float | int) -> type[CheckFailed]:
|
|
144
155
|
"""Request took too long."""
|
|
145
|
-
return _get_hashed_exception("TimeoutError", str(deadline))
|
|
156
|
+
return _get_hashed_exception(f"TimeoutError{prefix}", str(deadline))
|
|
146
157
|
|
|
147
158
|
|
|
148
159
|
def get_unexpected_graphql_response_error(type_: type) -> type[CheckFailed]:
|
|
@@ -158,7 +169,7 @@ def get_grouped_graphql_error(errors: list[GraphQLFormattedError]) -> type[Check
|
|
|
158
169
|
if "locations" in error:
|
|
159
170
|
message += ";locations:"
|
|
160
171
|
for location in sorted(error["locations"]):
|
|
161
|
-
message += f"({location['line'],location['column']})"
|
|
172
|
+
message += f"({location['line'], location['column']})"
|
|
162
173
|
if "path" in error:
|
|
163
174
|
message += ";path:"
|
|
164
175
|
for chunk in error["path"]:
|
|
@@ -196,7 +207,7 @@ class OperationSchemaError(Exception):
|
|
|
196
207
|
message = "Invalid schema definition"
|
|
197
208
|
error_path = " -> ".join(str(entry) for entry in error.path) or "[root]"
|
|
198
209
|
message += f"\n\nLocation:\n {error_path}"
|
|
199
|
-
instance =
|
|
210
|
+
instance = truncate_json(error.instance)
|
|
200
211
|
message += f"\n\nProblematic definition:\n{instance}"
|
|
201
212
|
message += "\n\nError details:\n "
|
|
202
213
|
# This default message contains the instance which we already printed
|
|
@@ -289,39 +300,6 @@ class InvalidHeadersExample(OperationSchemaError):
|
|
|
289
300
|
return cls(message)
|
|
290
301
|
|
|
291
302
|
|
|
292
|
-
def truncated_json(data: Any, max_lines: int = 10, max_width: int = 80) -> str:
|
|
293
|
-
# Convert JSON to string with indentation
|
|
294
|
-
indent = 4
|
|
295
|
-
serialized = json.dumps(data, indent=indent)
|
|
296
|
-
|
|
297
|
-
# Split string by lines
|
|
298
|
-
|
|
299
|
-
lines = [line[: max_width - 3] + "..." if len(line) > max_width else line for line in serialized.split("\n")]
|
|
300
|
-
|
|
301
|
-
if len(lines) <= max_lines:
|
|
302
|
-
return "\n".join(lines)
|
|
303
|
-
|
|
304
|
-
truncated_lines = lines[: max_lines - 1]
|
|
305
|
-
indentation = " " * indent
|
|
306
|
-
truncated_lines.append(f"{indentation}// Output truncated...")
|
|
307
|
-
truncated_lines.append(lines[-1])
|
|
308
|
-
|
|
309
|
-
return "\n".join(truncated_lines)
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
MAX_PAYLOAD_SIZE = 512
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
def prepare_response_payload(payload: str, max_size: int = MAX_PAYLOAD_SIZE) -> str:
|
|
316
|
-
if payload.endswith("\r\n"):
|
|
317
|
-
payload = payload[:-2]
|
|
318
|
-
elif payload.endswith("\n"):
|
|
319
|
-
payload = payload[:-1]
|
|
320
|
-
if len(payload) > max_size:
|
|
321
|
-
payload = payload[:max_size] + " // Output truncated..."
|
|
322
|
-
return payload
|
|
323
|
-
|
|
324
|
-
|
|
325
303
|
class DeadlineExceeded(Exception):
|
|
326
304
|
"""Test took too long to run."""
|
|
327
305
|
|
|
@@ -541,7 +519,7 @@ def remove_ssl_line_number(text: str) -> str:
|
|
|
541
519
|
|
|
542
520
|
|
|
543
521
|
def extract_requests_exception_details(exc: RequestException) -> tuple[str, list[str]]:
|
|
544
|
-
from requests.exceptions import
|
|
522
|
+
from requests.exceptions import ChunkedEncodingError, ConnectionError, SSLError
|
|
545
523
|
from urllib3.exceptions import MaxRetryError
|
|
546
524
|
|
|
547
525
|
if isinstance(exc, SSLError):
|
|
@@ -552,11 +530,14 @@ def extract_requests_exception_details(exc: RequestException) -> tuple[str, list
|
|
|
552
530
|
message = "Connection failed"
|
|
553
531
|
inner = exc.args[0]
|
|
554
532
|
if isinstance(inner, MaxRetryError) and inner.reason is not None:
|
|
555
|
-
arg =
|
|
556
|
-
if
|
|
557
|
-
|
|
533
|
+
arg = inner.reason.args[0]
|
|
534
|
+
if isinstance(arg, str):
|
|
535
|
+
if ":" not in arg:
|
|
536
|
+
reason = arg
|
|
537
|
+
else:
|
|
538
|
+
_, reason = arg.split(":", maxsplit=1)
|
|
558
539
|
else:
|
|
559
|
-
|
|
540
|
+
reason = f"Max retries exceeded with url: {inner.url}"
|
|
560
541
|
extra = [reason.strip()]
|
|
561
542
|
else:
|
|
562
543
|
extra = [" ".join(map(str, inner.args))]
|
|
@@ -79,3 +79,17 @@ SCHEMA_ANALYSIS = GLOBAL_EXPERIMENTS.create_experiment(
|
|
|
79
79
|
description="Analyzing API schemas via Schemathesis.io",
|
|
80
80
|
discussion_url="https://github.com/schemathesis/schemathesis/discussions/2056",
|
|
81
81
|
)
|
|
82
|
+
STATEFUL_TEST_RUNNER = GLOBAL_EXPERIMENTS.create_experiment(
|
|
83
|
+
name="stateful-test-runner",
|
|
84
|
+
verbose_name="New Stateful Test Runner",
|
|
85
|
+
env_var="STATEFUL_TEST_RUNNER",
|
|
86
|
+
description="State machine-based runner for stateful tests in CLI",
|
|
87
|
+
discussion_url="https://github.com/schemathesis/schemathesis/discussions/2262",
|
|
88
|
+
)
|
|
89
|
+
STATEFUL_ONLY = GLOBAL_EXPERIMENTS.create_experiment(
|
|
90
|
+
name="stateful-only",
|
|
91
|
+
verbose_name="Stateful Only",
|
|
92
|
+
env_var="STATEFUL_ONLY",
|
|
93
|
+
description="Run only stateful tests",
|
|
94
|
+
discussion_url="https://github.com/schemathesis/schemathesis/discussions/2262",
|
|
95
|
+
)
|
schemathesis/extra/_aiohttp.py
CHANGED
schemathesis/extra/_server.py
CHANGED
|
@@ -3,19 +3,18 @@ from __future__ import annotations
|
|
|
3
3
|
import unittest
|
|
4
4
|
from contextlib import contextmanager
|
|
5
5
|
from functools import partial
|
|
6
|
-
from typing import Any, Callable, Generator, Type,
|
|
6
|
+
from typing import Any, Callable, Generator, Type, cast
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
9
9
|
from _pytest import fixtures, nodes
|
|
10
10
|
from _pytest.config import hookimpl
|
|
11
11
|
from _pytest.fixtures import FuncFixtureInfo
|
|
12
|
-
from _pytest.nodes import Node
|
|
13
12
|
from _pytest.python import Class, Function, FunctionDefinition, Metafunc, Module, PyCollector
|
|
14
13
|
from hypothesis import reporting
|
|
15
14
|
from hypothesis.errors import InvalidArgument, Unsatisfiable
|
|
16
15
|
from jsonschema.exceptions import SchemaError
|
|
17
16
|
|
|
18
|
-
from .._dependency_versions import IS_PYTEST_ABOVE_7, IS_PYTEST_ABOVE_8
|
|
17
|
+
from .._dependency_versions import IS_PYTEST_ABOVE_7, IS_PYTEST_ABOVE_8
|
|
19
18
|
from .._override import get_override_from_mark
|
|
20
19
|
from ..constants import (
|
|
21
20
|
GIVEN_AND_EXPLICIT_EXAMPLES_ERROR_MESSAGE,
|
|
@@ -43,14 +42,6 @@ from ..utils import (
|
|
|
43
42
|
validate_given_args,
|
|
44
43
|
)
|
|
45
44
|
|
|
46
|
-
T = TypeVar("T", bound=Node)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def create(cls: type[T], *args: Any, **kwargs: Any) -> T:
|
|
50
|
-
if IS_PYTEST_ABOVE_54:
|
|
51
|
-
return cls.from_parent(*args, **kwargs) # type: ignore
|
|
52
|
-
return cls(*args, **kwargs)
|
|
53
|
-
|
|
54
45
|
|
|
55
46
|
class SchemathesisFunction(Function):
|
|
56
47
|
def __init__(
|
|
@@ -155,7 +146,9 @@ class SchemathesisCase(PyCollector):
|
|
|
155
146
|
name += f"[{error.full_path}]"
|
|
156
147
|
|
|
157
148
|
cls = self._get_class_parent()
|
|
158
|
-
definition: FunctionDefinition =
|
|
149
|
+
definition: FunctionDefinition = FunctionDefinition.from_parent(
|
|
150
|
+
name=self.name, parent=self.parent, callobj=funcobj
|
|
151
|
+
)
|
|
159
152
|
fixturemanager = self.session._fixturemanager
|
|
160
153
|
fixtureinfo = fixturemanager.getfixtureinfo(definition, funcobj, cls)
|
|
161
154
|
|
|
@@ -166,8 +159,7 @@ class SchemathesisCase(PyCollector):
|
|
|
166
159
|
funcobj = partial(funcobj, self.parent.obj)
|
|
167
160
|
|
|
168
161
|
if not metafunc._calls:
|
|
169
|
-
yield
|
|
170
|
-
SchemathesisFunction,
|
|
162
|
+
yield SchemathesisFunction.from_parent(
|
|
171
163
|
name=name,
|
|
172
164
|
parent=self.parent,
|
|
173
165
|
callobj=funcobj,
|
|
@@ -181,10 +173,9 @@ class SchemathesisCase(PyCollector):
|
|
|
181
173
|
fixtureinfo.prune_dependency_tree()
|
|
182
174
|
for callspec in metafunc._calls:
|
|
183
175
|
subname = f"{name}[{callspec.id}]"
|
|
184
|
-
yield
|
|
185
|
-
|
|
176
|
+
yield SchemathesisFunction.from_parent(
|
|
177
|
+
self.parent,
|
|
186
178
|
name=subname,
|
|
187
|
-
parent=self.parent,
|
|
188
179
|
callspec=callspec,
|
|
189
180
|
callobj=funcobj,
|
|
190
181
|
fixtureinfo=fixtureinfo,
|
|
@@ -236,7 +227,7 @@ def pytest_pycollect_makeitem(collector: nodes.Collector, name: str, obj: Any) -
|
|
|
236
227
|
"""Switch to a different collector if the test is parametrized marked by schemathesis."""
|
|
237
228
|
outcome = yield
|
|
238
229
|
if is_schemathesis_test(obj):
|
|
239
|
-
outcome.force_result(
|
|
230
|
+
outcome.force_result(SchemathesisCase.from_parent(collector, test_function=obj, name=name))
|
|
240
231
|
else:
|
|
241
232
|
outcome.get_result()
|
|
242
233
|
|
|
@@ -261,7 +252,7 @@ def skip_unnecessary_hypothesis_output() -> Generator:
|
|
|
261
252
|
yield
|
|
262
253
|
|
|
263
254
|
|
|
264
|
-
@hookimpl(
|
|
255
|
+
@hookimpl(wrapper=True)
|
|
265
256
|
def pytest_pyfunc_call(pyfuncitem): # type:ignore
|
|
266
257
|
"""It is possible to have a Hypothesis exception in runtime.
|
|
267
258
|
|
|
@@ -278,10 +269,9 @@ def pytest_pyfunc_call(pyfuncitem): # type:ignore
|
|
|
278
269
|
|
|
279
270
|
__tracebackhide__ = True
|
|
280
271
|
if isinstance(pyfuncitem, SchemathesisFunction):
|
|
281
|
-
with skip_unnecessary_hypothesis_output():
|
|
282
|
-
outcome = yield
|
|
283
272
|
try:
|
|
284
|
-
|
|
273
|
+
with skip_unnecessary_hypothesis_output():
|
|
274
|
+
yield
|
|
285
275
|
except InvalidArgument as exc:
|
|
286
276
|
if "Inconsistent args" in str(exc) and "@example()" in str(exc):
|
|
287
277
|
raise UsageError(GIVEN_AND_EXPLICIT_EXAMPLES_ERROR_MESSAGE) from None
|
|
@@ -316,5 +306,4 @@ def pytest_pyfunc_call(pyfuncitem): # type:ignore
|
|
|
316
306
|
if invalid_headers is not None:
|
|
317
307
|
raise InvalidHeadersExample.from_headers(invalid_headers) from None
|
|
318
308
|
else:
|
|
319
|
-
|
|
320
|
-
outcome.get_result()
|
|
309
|
+
yield
|
schemathesis/failures.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import textwrap
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from json import JSONDecodeError
|
|
5
|
-
from typing import
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from schemathesis.internal.output import OutputConfig
|
|
6
9
|
|
|
7
10
|
if TYPE_CHECKING:
|
|
8
11
|
from graphql.error import GraphQLFormattedError
|
|
@@ -41,16 +44,27 @@ class ValidationErrorContext(FailureContext):
|
|
|
41
44
|
return ("/".join(map(str, self.schema_path)),)
|
|
42
45
|
|
|
43
46
|
@classmethod
|
|
44
|
-
def from_exception(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
def from_exception(
|
|
48
|
+
cls, exc: ValidationError, *, output_config: OutputConfig | None = None
|
|
49
|
+
) -> ValidationErrorContext:
|
|
50
|
+
from .internal.output import truncate_json
|
|
51
|
+
|
|
52
|
+
output_config = OutputConfig.from_parent(output_config, max_lines=20)
|
|
53
|
+
schema = textwrap.indent(truncate_json(exc.schema, config=output_config), prefix=" ")
|
|
54
|
+
value = textwrap.indent(truncate_json(exc.instance, config=output_config), prefix=" ")
|
|
55
|
+
schema_path = list(exc.absolute_schema_path)
|
|
56
|
+
if len(schema_path) > 1:
|
|
57
|
+
# Exclude the last segment, which is already in the schema
|
|
58
|
+
schema_title = "Schema at "
|
|
59
|
+
for segment in schema_path[:-1]:
|
|
60
|
+
schema_title += f"/{segment}"
|
|
61
|
+
else:
|
|
62
|
+
schema_title = "Schema"
|
|
63
|
+
message = f"{exc.message}\n\n{schema_title}:\n\n{schema}\n\nValue:\n\n{value}"
|
|
50
64
|
return cls(
|
|
51
65
|
message=message,
|
|
52
66
|
validation_message=exc.message,
|
|
53
|
-
schema_path=
|
|
67
|
+
schema_path=schema_path,
|
|
54
68
|
schema=exc.schema,
|
|
55
69
|
instance_path=list(exc.absolute_path),
|
|
56
70
|
instance=exc.instance,
|
|
@@ -117,6 +131,26 @@ class UndefinedContentType(FailureContext):
|
|
|
117
131
|
type: str = "undefined_content_type"
|
|
118
132
|
|
|
119
133
|
|
|
134
|
+
@dataclass(repr=False)
|
|
135
|
+
class AcceptedNegativeData(FailureContext):
|
|
136
|
+
"""Response with negative data was accepted."""
|
|
137
|
+
|
|
138
|
+
message: str
|
|
139
|
+
title: str = "Accepted negative data"
|
|
140
|
+
type: str = "accepted_negative_data"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass(repr=False)
|
|
144
|
+
class UseAfterFree(FailureContext):
|
|
145
|
+
"""Resource was used after a successful DELETE operation on it."""
|
|
146
|
+
|
|
147
|
+
message: str
|
|
148
|
+
free: str
|
|
149
|
+
usage: str
|
|
150
|
+
title: str = "Use after free"
|
|
151
|
+
type: str = "use_after_free"
|
|
152
|
+
|
|
153
|
+
|
|
120
154
|
@dataclass(repr=False)
|
|
121
155
|
class UndefinedStatusCode(FailureContext):
|
|
122
156
|
"""Response has a status code that is not defined in the schema."""
|
schemathesis/filters.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"""Filtering system that allows users to filter API operations based on certain criteria."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
+
|
|
4
5
|
import re
|
|
5
6
|
from dataclasses import dataclass, field
|
|
6
7
|
from functools import partial
|
|
7
8
|
from types import SimpleNamespace
|
|
8
|
-
from typing import TYPE_CHECKING, Callable, List,
|
|
9
|
+
from typing import TYPE_CHECKING, Callable, List, Protocol, Union
|
|
9
10
|
|
|
10
11
|
from .exceptions import UsageError
|
|
11
12
|
|
schemathesis/fixups/__init__.py
CHANGED
schemathesis/fixups/fast_api.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
from typing import Any
|
|
3
4
|
|
|
4
|
-
from ..hooks import HookContext
|
|
5
|
+
from ..hooks import HookContext, register, unregister
|
|
5
6
|
from ..hooks import is_installed as global_is_installed
|
|
6
|
-
from ..hooks import register, unregister
|
|
7
7
|
from ..internal.jsonschema import traverse_schema
|
|
8
8
|
|
|
9
9
|
|
schemathesis/fixups/utf8_bom.py
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
from typing import TYPE_CHECKING
|
|
2
2
|
|
|
3
3
|
from ..constants import BOM_MARK
|
|
4
|
-
from ..hooks import HookContext
|
|
4
|
+
from ..hooks import HookContext, register, unregister
|
|
5
5
|
from ..hooks import is_installed as global_is_installed
|
|
6
|
-
from ..hooks import register, unregister
|
|
7
6
|
|
|
8
7
|
if TYPE_CHECKING:
|
|
9
8
|
from ..models import Case
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import random
|
|
3
4
|
from dataclasses import dataclass, field
|
|
4
5
|
from enum import Enum
|
|
5
|
-
from typing import
|
|
6
|
+
from typing import TYPE_CHECKING, Iterable, Union
|
|
6
7
|
|
|
7
8
|
if TYPE_CHECKING:
|
|
8
9
|
from hypothesis.strategies import SearchStrategy
|
schemathesis/hooks.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import inspect
|
|
3
4
|
from collections import defaultdict
|
|
4
5
|
from copy import deepcopy
|
|
@@ -7,11 +8,12 @@ from enum import Enum, unique
|
|
|
7
8
|
from functools import partial
|
|
8
9
|
from typing import TYPE_CHECKING, Any, Callable, ClassVar, DefaultDict, cast
|
|
9
10
|
|
|
10
|
-
from .types import GenericTest
|
|
11
11
|
from .internal.deprecation import deprecated_property
|
|
12
|
+
from .types import GenericTest
|
|
12
13
|
|
|
13
14
|
if TYPE_CHECKING:
|
|
14
15
|
from hypothesis import strategies as st
|
|
16
|
+
|
|
15
17
|
from .models import APIOperation, Case
|
|
16
18
|
from .schemas import BaseSchema
|
|
17
19
|
from .transports.responses import GenericResponse
|
schemathesis/internal/copy.py
CHANGED
|
@@ -2,12 +2,28 @@ from typing import Any
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
def fast_deepcopy(value: Any) -> Any:
|
|
5
|
-
"""A specialized version of `deepcopy` that copies only `dict` and `list
|
|
5
|
+
"""A specialized version of `deepcopy` that copies only `dict` and `list` and does unrolling.
|
|
6
6
|
|
|
7
7
|
It is on average 3x faster than `deepcopy` and given the amount of calls, it is an important optimization.
|
|
8
8
|
"""
|
|
9
9
|
if isinstance(value, dict):
|
|
10
|
-
return {
|
|
10
|
+
return {
|
|
11
|
+
k1: (
|
|
12
|
+
{k2: fast_deepcopy(v2) for k2, v2 in v1.items()}
|
|
13
|
+
if isinstance(v1, dict)
|
|
14
|
+
else [fast_deepcopy(v2) for v2 in v1]
|
|
15
|
+
if isinstance(v1, list)
|
|
16
|
+
else v1
|
|
17
|
+
)
|
|
18
|
+
for k1, v1 in value.items()
|
|
19
|
+
}
|
|
11
20
|
if isinstance(value, list):
|
|
12
|
-
return [
|
|
21
|
+
return [
|
|
22
|
+
{k2: fast_deepcopy(v2) for k2, v2 in v1.items()}
|
|
23
|
+
if isinstance(v1, dict)
|
|
24
|
+
else [fast_deepcopy(v2) for v2 in v1]
|
|
25
|
+
if isinstance(v1, list)
|
|
26
|
+
else v1
|
|
27
|
+
for v1 in value
|
|
28
|
+
]
|
|
13
29
|
return value
|