schemathesis 3.25.6__py3-none-any.whl → 3.39.7__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 +6 -6
- schemathesis/_compat.py +2 -2
- schemathesis/_dependency_versions.py +4 -2
- schemathesis/_hypothesis.py +369 -56
- schemathesis/_lazy_import.py +1 -0
- schemathesis/_override.py +5 -4
- schemathesis/_patches.py +21 -0
- schemathesis/_rate_limiter.py +7 -0
- schemathesis/_xml.py +75 -22
- schemathesis/auths.py +78 -16
- schemathesis/checks.py +21 -9
- schemathesis/cli/__init__.py +783 -432
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/callbacks.py +58 -13
- schemathesis/cli/cassettes.py +233 -47
- schemathesis/cli/constants.py +8 -2
- schemathesis/cli/context.py +22 -5
- schemathesis/cli/debug.py +2 -1
- schemathesis/cli/handlers.py +4 -1
- schemathesis/cli/junitxml.py +103 -22
- schemathesis/cli/options.py +15 -4
- schemathesis/cli/output/default.py +258 -112
- schemathesis/cli/output/short.py +23 -8
- schemathesis/cli/reporting.py +79 -0
- schemathesis/cli/sanitization.py +6 -0
- schemathesis/code_samples.py +5 -3
- schemathesis/constants.py +1 -0
- schemathesis/contrib/openapi/__init__.py +1 -1
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
- schemathesis/contrib/openapi/formats/uuid.py +2 -1
- schemathesis/contrib/unique_data.py +3 -3
- schemathesis/exceptions.py +76 -65
- schemathesis/experimental/__init__.py +35 -0
- schemathesis/extra/_aiohttp.py +1 -0
- schemathesis/extra/_flask.py +4 -1
- schemathesis/extra/_server.py +1 -0
- schemathesis/extra/pytest_plugin.py +17 -25
- schemathesis/failures.py +77 -9
- schemathesis/filters.py +185 -8
- schemathesis/fixups/__init__.py +1 -0
- schemathesis/fixups/fast_api.py +2 -2
- schemathesis/fixups/utf8_bom.py +1 -2
- schemathesis/generation/__init__.py +20 -36
- schemathesis/generation/_hypothesis.py +59 -0
- schemathesis/generation/_methods.py +44 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/graphql.py +0 -1
- schemathesis/hooks.py +89 -12
- schemathesis/internal/checks.py +84 -0
- schemathesis/internal/copy.py +22 -3
- schemathesis/internal/deprecation.py +6 -2
- schemathesis/internal/diff.py +15 -0
- schemathesis/internal/extensions.py +27 -0
- schemathesis/internal/jsonschema.py +2 -1
- schemathesis/internal/output.py +68 -0
- schemathesis/internal/result.py +1 -1
- schemathesis/internal/transformation.py +11 -0
- schemathesis/lazy.py +138 -25
- schemathesis/loaders.py +7 -5
- schemathesis/models.py +318 -211
- schemathesis/parameters.py +4 -0
- schemathesis/runner/__init__.py +50 -15
- schemathesis/runner/events.py +65 -5
- schemathesis/runner/impl/context.py +104 -0
- schemathesis/runner/impl/core.py +388 -177
- schemathesis/runner/impl/solo.py +19 -29
- schemathesis/runner/impl/threadpool.py +70 -79
- schemathesis/runner/probes.py +11 -9
- schemathesis/runner/serialization.py +150 -17
- schemathesis/sanitization.py +5 -1
- schemathesis/schemas.py +170 -102
- schemathesis/serializers.py +7 -2
- schemathesis/service/ci.py +1 -0
- schemathesis/service/client.py +39 -6
- schemathesis/service/events.py +5 -1
- schemathesis/service/extensions.py +224 -0
- schemathesis/service/hosts.py +6 -2
- schemathesis/service/metadata.py +25 -0
- schemathesis/service/models.py +211 -2
- schemathesis/service/report.py +6 -6
- schemathesis/service/serialization.py +45 -71
- schemathesis/service/usage.py +1 -0
- schemathesis/specs/graphql/_cache.py +26 -0
- schemathesis/specs/graphql/loaders.py +25 -5
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +2 -2
- schemathesis/specs/graphql/schemas.py +130 -100
- schemathesis/specs/graphql/validation.py +1 -2
- schemathesis/specs/openapi/__init__.py +1 -0
- schemathesis/specs/openapi/_cache.py +123 -0
- schemathesis/specs/openapi/_hypothesis.py +78 -60
- schemathesis/specs/openapi/checks.py +504 -25
- schemathesis/specs/openapi/converter.py +31 -4
- schemathesis/specs/openapi/definitions.py +10 -17
- schemathesis/specs/openapi/examples.py +126 -12
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +1 -1
- schemathesis/specs/openapi/expressions/extractors.py +26 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +29 -6
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +44 -0
- schemathesis/specs/openapi/links.py +125 -42
- schemathesis/specs/openapi/loaders.py +77 -36
- schemathesis/specs/openapi/media_types.py +34 -0
- schemathesis/specs/openapi/negative/__init__.py +6 -3
- schemathesis/specs/openapi/negative/mutations.py +21 -6
- schemathesis/specs/openapi/parameters.py +39 -25
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +37 -7
- schemathesis/specs/openapi/schemas.py +360 -241
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +1 -0
- schemathesis/specs/openapi/stateful/__init__.py +198 -70
- schemathesis/specs/openapi/stateful/statistic.py +198 -0
- schemathesis/specs/openapi/stateful/types.py +14 -0
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/specs/openapi/validation.py +1 -0
- schemathesis/stateful/__init__.py +35 -21
- schemathesis/stateful/config.py +97 -0
- schemathesis/stateful/context.py +135 -0
- schemathesis/stateful/events.py +274 -0
- schemathesis/stateful/runner.py +309 -0
- schemathesis/stateful/sink.py +68 -0
- schemathesis/stateful/state_machine.py +67 -38
- schemathesis/stateful/statistic.py +22 -0
- schemathesis/stateful/validation.py +100 -0
- schemathesis/targets.py +33 -1
- schemathesis/throttling.py +25 -5
- schemathesis/transports/__init__.py +354 -0
- schemathesis/transports/asgi.py +7 -0
- schemathesis/transports/auth.py +25 -2
- schemathesis/transports/content_types.py +3 -1
- schemathesis/transports/headers.py +2 -1
- schemathesis/transports/responses.py +9 -4
- schemathesis/types.py +9 -0
- schemathesis/utils.py +11 -16
- schemathesis-3.39.7.dist-info/METADATA +293 -0
- schemathesis-3.39.7.dist-info/RECORD +160 -0
- {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis-3.25.6.dist-info/METADATA +0 -356
- schemathesis-3.25.6.dist-info/RECORD +0 -134
- {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from itertools import groupby
|
|
4
|
+
from typing import TYPE_CHECKING, Callable, Generator, Iterator
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from ..exceptions import RuntimeErrorType
|
|
9
|
+
from ..runner.serialization import SerializedCheck, deduplicate_failures
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ..code_samples import CodeSampleStyle
|
|
13
|
+
|
|
14
|
+
TEST_CASE_ID_TITLE = "Test Case ID"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def group_by_case(
|
|
18
|
+
checks: list[SerializedCheck], code_sample_style: CodeSampleStyle
|
|
19
|
+
) -> Generator[tuple[str, Iterator[SerializedCheck]], None, None]:
|
|
20
|
+
checks = deduplicate_failures(checks)
|
|
21
|
+
checks = sorted(checks, key=lambda c: _by_unique_key(c, code_sample_style))
|
|
22
|
+
for (sample, _, _), gen in groupby(checks, lambda c: _by_unique_key(c, code_sample_style)):
|
|
23
|
+
yield (sample, gen)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _by_unique_key(check: SerializedCheck, code_sample_style: CodeSampleStyle) -> tuple[str, int, str]:
|
|
27
|
+
return (
|
|
28
|
+
code_sample_style.generate(
|
|
29
|
+
method=check.example.method,
|
|
30
|
+
url=check.example.url,
|
|
31
|
+
body=check.example.deserialize_body(),
|
|
32
|
+
headers=check.example.headers,
|
|
33
|
+
verify=check.example.verify,
|
|
34
|
+
extra_headers=check.example.extra_headers,
|
|
35
|
+
),
|
|
36
|
+
0 if not check.response else check.response.status_code,
|
|
37
|
+
"SCHEMATHESIS-INTERNAL-NO-RESPONSE"
|
|
38
|
+
if not check.response
|
|
39
|
+
else check.response.body or "SCHEMATHESIS-INTERNAL-EMPTY-BODY",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def split_traceback(traceback: str) -> list[str]:
|
|
44
|
+
return [entry for entry in traceback.splitlines() if entry]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def bold(option: str) -> str:
|
|
48
|
+
return click.style(option, bold=True)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_runtime_error_suggestion(error_type: RuntimeErrorType, bold: Callable[[str], str] = bold) -> str | None:
|
|
52
|
+
DISABLE_SSL_SUGGESTION = f"Bypass SSL verification with {bold('`--request-tls-verify=false`')}."
|
|
53
|
+
DISABLE_SCHEMA_VALIDATION_SUGGESTION = (
|
|
54
|
+
f"Bypass validation using {bold('`--validate-schema=false`')}. Caution: May cause unexpected errors."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def _format_health_check_suggestion(label: str) -> str:
|
|
58
|
+
return f"Bypass this health check using {bold(f'`--hypothesis-suppress-health-check={label}`')}."
|
|
59
|
+
|
|
60
|
+
RUNTIME_ERROR_SUGGESTIONS = {
|
|
61
|
+
RuntimeErrorType.CONNECTION_SSL: DISABLE_SSL_SUGGESTION,
|
|
62
|
+
RuntimeErrorType.HYPOTHESIS_DEADLINE_EXCEEDED: (
|
|
63
|
+
f"Adjust the deadline using {bold('`--hypothesis-deadline=MILLIS`')} or "
|
|
64
|
+
f"disable with {bold('`--hypothesis-deadline=None`')}."
|
|
65
|
+
),
|
|
66
|
+
RuntimeErrorType.HYPOTHESIS_UNSATISFIABLE: "Examine the schema for inconsistencies and consider simplifying it.",
|
|
67
|
+
RuntimeErrorType.SCHEMA_BODY_IN_GET_REQUEST: DISABLE_SCHEMA_VALIDATION_SUGGESTION,
|
|
68
|
+
RuntimeErrorType.SCHEMA_INVALID_REGULAR_EXPRESSION: "Ensure your regex is compatible with Python's syntax.\n"
|
|
69
|
+
"For guidance, visit: https://docs.python.org/3/library/re.html",
|
|
70
|
+
RuntimeErrorType.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR: "Define a custom strategy for it.\n"
|
|
71
|
+
"For guidance, visit: https://schemathesis.readthedocs.io/en/stable/graphql.html#custom-scalars",
|
|
72
|
+
RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE: _format_health_check_suggestion("data_too_large"),
|
|
73
|
+
RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH: _format_health_check_suggestion("filter_too_much"),
|
|
74
|
+
RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW: _format_health_check_suggestion("too_slow"),
|
|
75
|
+
RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE: _format_health_check_suggestion(
|
|
76
|
+
"large_base_example"
|
|
77
|
+
),
|
|
78
|
+
}
|
|
79
|
+
return RUNTIME_ERROR_SUGGESTIONS.get(error_type)
|
schemathesis/cli/sanitization.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
from dataclasses import dataclass
|
|
3
4
|
from typing import TYPE_CHECKING
|
|
4
5
|
|
|
@@ -18,3 +19,8 @@ class SanitizationHandler(EventHandler):
|
|
|
18
19
|
sanitize_serialized_check(check)
|
|
19
20
|
for interaction in event.result.interactions:
|
|
20
21
|
sanitize_serialized_interaction(interaction)
|
|
22
|
+
elif isinstance(event, events.AfterStatefulExecution):
|
|
23
|
+
for check in event.result.checks:
|
|
24
|
+
sanitize_serialized_check(check)
|
|
25
|
+
for interaction in event.result.interactions:
|
|
26
|
+
sanitize_serialized_interaction(interaction)
|
schemathesis/code_samples.py
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
from enum import Enum
|
|
3
4
|
from functools import lru_cache
|
|
4
5
|
from shlex import quote
|
|
5
6
|
from typing import TYPE_CHECKING
|
|
6
7
|
|
|
7
8
|
from .constants import SCHEMATHESIS_TEST_CASE_HEADER
|
|
8
|
-
from .types import Headers
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
11
|
from requests.structures import CaseInsensitiveDict
|
|
12
12
|
|
|
13
|
+
from .types import Headers
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
@lru_cache
|
|
15
17
|
def get_excluded_headers() -> CaseInsensitiveDict:
|
|
@@ -119,9 +121,9 @@ def _generate_requests(
|
|
|
119
121
|
url = _escape_single_quotes(url)
|
|
120
122
|
command = f"requests.{method.lower()}('{url}'"
|
|
121
123
|
if body:
|
|
122
|
-
command += f", data={
|
|
124
|
+
command += f", data={body!r}"
|
|
123
125
|
if headers:
|
|
124
|
-
command += f", headers={
|
|
126
|
+
command += f", headers={headers!r}"
|
|
125
127
|
if not verify:
|
|
126
128
|
command += ", verify=False"
|
|
127
129
|
command += ")"
|
schemathesis/constants.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
3
5
|
from ...hooks import HookContext, register, unregister
|
|
4
6
|
|
|
5
7
|
if TYPE_CHECKING:
|
|
@@ -16,7 +18,7 @@ def uninstall() -> None:
|
|
|
16
18
|
|
|
17
19
|
def before_add_examples(context: HookContext, examples: list[Case]) -> None:
|
|
18
20
|
if not examples and context.operation is not None:
|
|
19
|
-
from ...
|
|
21
|
+
from ...generation import add_single_example
|
|
20
22
|
|
|
21
23
|
strategy = context.operation.as_strategy()
|
|
22
24
|
add_single_example(strategy, examples)
|
|
@@ -6,14 +6,14 @@ 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(
|
|
15
|
-
"The
|
|
16
|
-
"are **DEPRECATED**. The concept of this feature does not fit the core principles of Hypothesis where "
|
|
16
|
+
"The `schemathesis.contrib.unique_data` hook is **DEPRECATED**. The concept of this feature does not fit the core principles of Hypothesis where "
|
|
17
17
|
"strategies are configurable on a per-example basis but this feature implies uniqueness across examples. "
|
|
18
18
|
"This leads to cryptic error messages about external state and flaky test runs, "
|
|
19
19
|
"therefore it will be removed in Schemathesis 4.0",
|
schemathesis/exceptions.py
CHANGED
|
@@ -1,24 +1,28 @@
|
|
|
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
|
|
7
7
|
from hashlib import sha1
|
|
8
|
-
from json import JSONDecodeError
|
|
9
|
-
from types import TracebackType
|
|
10
8
|
from typing import TYPE_CHECKING, Any, Callable, Generator, NoReturn
|
|
11
9
|
|
|
12
10
|
from .constants import SERIALIZERS_SUGGESTION_MESSAGE
|
|
13
|
-
from .
|
|
11
|
+
from .internal.output import truncate_json
|
|
14
12
|
|
|
15
13
|
if TYPE_CHECKING:
|
|
14
|
+
from json import JSONDecodeError
|
|
15
|
+
from types import TracebackType
|
|
16
|
+
|
|
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 .failures import FailureContext
|
|
24
|
+
from .transports.responses import GenericResponse
|
|
25
|
+
|
|
22
26
|
|
|
23
27
|
class CheckFailed(AssertionError):
|
|
24
28
|
"""Custom error type to distinguish from arbitrary AssertionError that may happen in the dependent libraries."""
|
|
@@ -97,52 +101,72 @@ def get_grouped_exception(prefix: str, *exceptions: AssertionError) -> type[Chec
|
|
|
97
101
|
return _get_hashed_exception("GroupedException", f"{prefix}{message}")
|
|
98
102
|
|
|
99
103
|
|
|
100
|
-
def get_server_error(status_code: int) -> type[CheckFailed]:
|
|
104
|
+
def get_server_error(prefix: str, status_code: int) -> type[CheckFailed]:
|
|
101
105
|
"""Return new exception for the Internal Server Error cases."""
|
|
102
|
-
name = f"ServerError{status_code}"
|
|
106
|
+
name = f"ServerError{prefix}{status_code}"
|
|
103
107
|
return get_exception(name)
|
|
104
108
|
|
|
105
109
|
|
|
106
|
-
def get_status_code_error(status_code: int) -> type[CheckFailed]:
|
|
110
|
+
def get_status_code_error(prefix: str, status_code: int) -> type[CheckFailed]:
|
|
107
111
|
"""Return new exception for an unexpected status code."""
|
|
108
|
-
name = f"StatusCodeError{status_code}"
|
|
112
|
+
name = f"StatusCodeError{prefix}{status_code}"
|
|
109
113
|
return get_exception(name)
|
|
110
114
|
|
|
111
115
|
|
|
112
|
-
def get_response_type_error(expected: str, received: str) -> type[CheckFailed]:
|
|
116
|
+
def get_response_type_error(prefix: str, expected: str, received: str) -> type[CheckFailed]:
|
|
113
117
|
"""Return new exception for an unexpected response type."""
|
|
114
|
-
name = f"SchemaValidationError{expected}_{received}"
|
|
118
|
+
name = f"SchemaValidationError{prefix}{expected}_{received}"
|
|
115
119
|
return get_exception(name)
|
|
116
120
|
|
|
117
121
|
|
|
118
|
-
def get_malformed_media_type_error(media_type: str) -> type[CheckFailed]:
|
|
119
|
-
name = f"MalformedMediaType{media_type}"
|
|
122
|
+
def get_malformed_media_type_error(prefix: str, media_type: str) -> type[CheckFailed]:
|
|
123
|
+
name = f"MalformedMediaType{prefix}{media_type}"
|
|
120
124
|
return get_exception(name)
|
|
121
125
|
|
|
122
126
|
|
|
123
|
-
def get_missing_content_type_error() -> type[CheckFailed]:
|
|
127
|
+
def get_missing_content_type_error(prefix: str) -> type[CheckFailed]:
|
|
124
128
|
"""Return new exception for a missing Content-Type header."""
|
|
125
|
-
return get_exception("MissingContentTypeError")
|
|
129
|
+
return get_exception(f"MissingContentTypeError{prefix}")
|
|
126
130
|
|
|
127
131
|
|
|
128
|
-
def get_schema_validation_error(exception: ValidationError) -> type[CheckFailed]:
|
|
132
|
+
def get_schema_validation_error(prefix: str, exception: ValidationError) -> type[CheckFailed]:
|
|
129
133
|
"""Return new exception for schema validation error."""
|
|
130
|
-
return _get_hashed_exception("SchemaValidationError", str(exception))
|
|
134
|
+
return _get_hashed_exception(f"SchemaValidationError{prefix}", str(exception))
|
|
131
135
|
|
|
132
136
|
|
|
133
|
-
def get_response_parsing_error(exception: JSONDecodeError) -> type[CheckFailed]:
|
|
137
|
+
def get_response_parsing_error(prefix: str, exception: JSONDecodeError) -> type[CheckFailed]:
|
|
134
138
|
"""Return new exception for response parsing error."""
|
|
135
|
-
return _get_hashed_exception("ResponseParsingError", str(exception))
|
|
139
|
+
return _get_hashed_exception(f"ResponseParsingError{prefix}", str(exception))
|
|
136
140
|
|
|
137
141
|
|
|
138
|
-
def get_headers_error(message: str) -> type[CheckFailed]:
|
|
142
|
+
def get_headers_error(prefix: str, message: str) -> type[CheckFailed]:
|
|
139
143
|
"""Return new exception for missing headers."""
|
|
140
|
-
return _get_hashed_exception("MissingHeadersError", message)
|
|
144
|
+
return _get_hashed_exception(f"MissingHeadersError{prefix}", message)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def get_negative_rejection_error(prefix: str, status: int) -> type[CheckFailed]:
|
|
148
|
+
return _get_hashed_exception(f"AcceptedNegativeDataError{prefix}", str(status))
|
|
149
|
+
|
|
141
150
|
|
|
151
|
+
def get_positive_acceptance_error(prefix: str, status: int) -> type[CheckFailed]:
|
|
152
|
+
return _get_hashed_exception(f"RejectedPositiveDataError{prefix}", str(status))
|
|
142
153
|
|
|
143
|
-
|
|
154
|
+
|
|
155
|
+
def get_use_after_free_error(free: str) -> type[CheckFailed]:
|
|
156
|
+
return _get_hashed_exception("UseAfterFreeError", free)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def get_ensure_resource_availability_error(operation: str) -> type[CheckFailed]:
|
|
160
|
+
return _get_hashed_exception("EnsureResourceAvailabilityError", operation)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def get_ignored_auth_error(operation: str) -> type[CheckFailed]:
|
|
164
|
+
return _get_hashed_exception("IgnoredAuthError", operation)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def get_timeout_error(prefix: str, deadline: float | int) -> type[CheckFailed]:
|
|
144
168
|
"""Request took too long."""
|
|
145
|
-
return _get_hashed_exception("TimeoutError", str(deadline))
|
|
169
|
+
return _get_hashed_exception(f"TimeoutError{prefix}", str(deadline))
|
|
146
170
|
|
|
147
171
|
|
|
148
172
|
def get_unexpected_graphql_response_error(type_: type) -> type[CheckFailed]:
|
|
@@ -158,7 +182,7 @@ def get_grouped_graphql_error(errors: list[GraphQLFormattedError]) -> type[Check
|
|
|
158
182
|
if "locations" in error:
|
|
159
183
|
message += ";locations:"
|
|
160
184
|
for location in sorted(error["locations"]):
|
|
161
|
-
message += f"({location['line'],location['column']})"
|
|
185
|
+
message += f"({location['line'], location['column']})"
|
|
162
186
|
if "path" in error:
|
|
163
187
|
message += ";path:"
|
|
164
188
|
for chunk in error["path"]:
|
|
@@ -196,7 +220,7 @@ class OperationSchemaError(Exception):
|
|
|
196
220
|
message = "Invalid schema definition"
|
|
197
221
|
error_path = " -> ".join(str(entry) for entry in error.path) or "[root]"
|
|
198
222
|
message += f"\n\nLocation:\n {error_path}"
|
|
199
|
-
instance =
|
|
223
|
+
instance = truncate_json(error.instance)
|
|
200
224
|
message += f"\n\nProblematic definition:\n{instance}"
|
|
201
225
|
message += "\n\nError details:\n "
|
|
202
226
|
# This default message contains the instance which we already printed
|
|
@@ -211,9 +235,11 @@ class OperationSchemaError(Exception):
|
|
|
211
235
|
def from_reference_resolution_error(
|
|
212
236
|
cls, error: RefResolutionError, path: str | None, method: str | None, full_path: str | None
|
|
213
237
|
) -> OperationSchemaError:
|
|
238
|
+
notes = getattr(error, "__notes__", [])
|
|
239
|
+
# Some exceptions don't have the actual reference in them, hence we add it manually via notes
|
|
240
|
+
pointer = f"'{notes[0]}'"
|
|
214
241
|
message = "Unresolvable JSON pointer in the schema"
|
|
215
242
|
# Get the pointer value from "Unresolvable JSON pointer: 'components/UnknownParameter'"
|
|
216
|
-
pointer = str(error).split(": ", 1)[-1]
|
|
217
243
|
message += f"\n\nError details:\n JSON pointer: {pointer}"
|
|
218
244
|
message += "\n This typically means that the schema is referencing a component that doesn't exist."
|
|
219
245
|
message += f"\n\n{SCHEMA_ERROR_SUGGESTION}"
|
|
@@ -289,39 +315,6 @@ class InvalidHeadersExample(OperationSchemaError):
|
|
|
289
315
|
return cls(message)
|
|
290
316
|
|
|
291
317
|
|
|
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
318
|
class DeadlineExceeded(Exception):
|
|
326
319
|
"""Test took too long to run."""
|
|
327
320
|
|
|
@@ -336,6 +329,12 @@ class DeadlineExceeded(Exception):
|
|
|
336
329
|
)
|
|
337
330
|
|
|
338
331
|
|
|
332
|
+
class RecursiveReferenceError(Exception):
|
|
333
|
+
"""Recursive reference is impossible to resolve due to current limitations."""
|
|
334
|
+
|
|
335
|
+
__module__ = "builtins"
|
|
336
|
+
|
|
337
|
+
|
|
339
338
|
@enum.unique
|
|
340
339
|
class RuntimeErrorType(str, enum.Enum):
|
|
341
340
|
# Connection related issues
|
|
@@ -354,6 +353,7 @@ class RuntimeErrorType(str, enum.Enum):
|
|
|
354
353
|
|
|
355
354
|
SCHEMA_BODY_IN_GET_REQUEST = "schema_body_in_get_request"
|
|
356
355
|
SCHEMA_INVALID_REGULAR_EXPRESSION = "schema_invalid_regular_expression"
|
|
356
|
+
SCHEMA_UNSUPPORTED = "schema_unsupported"
|
|
357
357
|
SCHEMA_GENERIC = "schema_generic"
|
|
358
358
|
|
|
359
359
|
SERIALIZATION_NOT_POSSIBLE = "serialization_not_possible"
|
|
@@ -367,6 +367,7 @@ class RuntimeErrorType(str, enum.Enum):
|
|
|
367
367
|
return self not in (
|
|
368
368
|
RuntimeErrorType.SCHEMA_BODY_IN_GET_REQUEST,
|
|
369
369
|
RuntimeErrorType.SCHEMA_INVALID_REGULAR_EXPRESSION,
|
|
370
|
+
RuntimeErrorType.SCHEMA_UNSUPPORTED,
|
|
370
371
|
RuntimeErrorType.SCHEMA_GENERIC,
|
|
371
372
|
RuntimeErrorType.SERIALIZATION_NOT_POSSIBLE,
|
|
372
373
|
)
|
|
@@ -532,8 +533,14 @@ def remove_ssl_line_number(text: str) -> str:
|
|
|
532
533
|
return re.sub(r"\(_ssl\.c:\d+\)", "", text)
|
|
533
534
|
|
|
534
535
|
|
|
536
|
+
def _clean_inner_request_message(message: Any) -> str:
|
|
537
|
+
if isinstance(message, str) and message.startswith("HTTPConnectionPool"):
|
|
538
|
+
return re.sub(r"HTTPConnectionPool\(.+?\): ", "", message).rstrip(".")
|
|
539
|
+
return str(message)
|
|
540
|
+
|
|
541
|
+
|
|
535
542
|
def extract_requests_exception_details(exc: RequestException) -> tuple[str, list[str]]:
|
|
536
|
-
from requests.exceptions import
|
|
543
|
+
from requests.exceptions import ChunkedEncodingError, ConnectionError, SSLError
|
|
537
544
|
from urllib3.exceptions import MaxRetryError
|
|
538
545
|
|
|
539
546
|
if isinstance(exc, SSLError):
|
|
@@ -544,13 +551,17 @@ def extract_requests_exception_details(exc: RequestException) -> tuple[str, list
|
|
|
544
551
|
message = "Connection failed"
|
|
545
552
|
inner = exc.args[0]
|
|
546
553
|
if isinstance(inner, MaxRetryError) and inner.reason is not None:
|
|
547
|
-
|
|
548
|
-
|
|
554
|
+
arg = inner.reason.args[0]
|
|
555
|
+
if isinstance(arg, str):
|
|
556
|
+
if ":" not in arg:
|
|
557
|
+
reason = arg
|
|
558
|
+
else:
|
|
559
|
+
_, reason = arg.split(":", maxsplit=1)
|
|
549
560
|
else:
|
|
550
|
-
|
|
561
|
+
reason = f"Max retries exceeded with url: {inner.url}"
|
|
551
562
|
extra = [reason.strip()]
|
|
552
563
|
else:
|
|
553
|
-
extra = [" ".join(map(
|
|
564
|
+
extra = [" ".join(map(_clean_inner_request_message, inner.args))]
|
|
554
565
|
elif isinstance(exc, ChunkedEncodingError):
|
|
555
566
|
message = "Connection broken. The server declared chunked encoding but sent an invalid chunk"
|
|
556
567
|
extra = [str(exc.args[0].args[1])]
|
|
@@ -72,3 +72,38 @@ OPEN_API_3_1 = GLOBAL_EXPERIMENTS.create_experiment(
|
|
|
72
72
|
description="Support for response validation",
|
|
73
73
|
discussion_url="https://github.com/schemathesis/schemathesis/discussions/1822",
|
|
74
74
|
)
|
|
75
|
+
SCHEMA_ANALYSIS = GLOBAL_EXPERIMENTS.create_experiment(
|
|
76
|
+
name="schema-analysis",
|
|
77
|
+
verbose_name="Schema Analysis",
|
|
78
|
+
env_var="SCHEMA_ANALYSIS",
|
|
79
|
+
description="Analyzing API schemas via Schemathesis.io",
|
|
80
|
+
discussion_url="https://github.com/schemathesis/schemathesis/discussions/2056",
|
|
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
|
+
)
|
|
96
|
+
COVERAGE_PHASE = GLOBAL_EXPERIMENTS.create_experiment(
|
|
97
|
+
name="coverage-phase",
|
|
98
|
+
verbose_name="Coverage phase",
|
|
99
|
+
env_var="COVERAGE_PHASE",
|
|
100
|
+
description="Generate covering test cases",
|
|
101
|
+
discussion_url="https://github.com/schemathesis/schemathesis/discussions/2418",
|
|
102
|
+
)
|
|
103
|
+
POSITIVE_DATA_ACCEPTANCE = GLOBAL_EXPERIMENTS.create_experiment(
|
|
104
|
+
name="positive_data_acceptance",
|
|
105
|
+
verbose_name="Positive Data Acceptance",
|
|
106
|
+
env_var="POSITIVE_DATA_ACCEPTANCE",
|
|
107
|
+
description="Verifying schema-conformant data is accepted",
|
|
108
|
+
discussion_url="https://github.com/schemathesis/schemathesis/discussions/2499",
|
|
109
|
+
)
|
schemathesis/extra/_aiohttp.py
CHANGED
schemathesis/extra/_flask.py
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
from . import _server
|
|
6
6
|
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from flask import Flask
|
|
9
|
+
|
|
7
10
|
|
|
8
11
|
def run_server(app: Flask, port: int | None = None, timeout: float = 0.05) -> int:
|
|
9
12
|
"""Start a thread with the given aiohttp application."""
|
schemathesis/extra/_server.py
CHANGED
|
@@ -3,19 +3,17 @@ 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 TYPE_CHECKING, 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
|
-
from _pytest.fixtures import FuncFixtureInfo
|
|
12
|
-
from _pytest.nodes import Node
|
|
13
11
|
from _pytest.python import Class, Function, FunctionDefinition, Metafunc, Module, PyCollector
|
|
14
12
|
from hypothesis import reporting
|
|
15
13
|
from hypothesis.errors import InvalidArgument, Unsatisfiable
|
|
16
14
|
from jsonschema.exceptions import SchemaError
|
|
17
15
|
|
|
18
|
-
from .._dependency_versions import IS_PYTEST_ABOVE_7, IS_PYTEST_ABOVE_8
|
|
16
|
+
from .._dependency_versions import IS_PYTEST_ABOVE_7, IS_PYTEST_ABOVE_8
|
|
19
17
|
from .._override import get_override_from_mark
|
|
20
18
|
from ..constants import (
|
|
21
19
|
GIVEN_AND_EXPLICIT_EXAMPLES_ERROR_MESSAGE,
|
|
@@ -31,7 +29,6 @@ from ..exceptions import (
|
|
|
31
29
|
UsageError,
|
|
32
30
|
)
|
|
33
31
|
from ..internal.result import Ok, Result
|
|
34
|
-
from ..models import APIOperation
|
|
35
32
|
from ..utils import (
|
|
36
33
|
PARAMETRIZE_MARKER,
|
|
37
34
|
fail_on_no_matches,
|
|
@@ -43,13 +40,10 @@ from ..utils import (
|
|
|
43
40
|
validate_given_args,
|
|
44
41
|
)
|
|
45
42
|
|
|
46
|
-
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from _pytest.fixtures import FuncFixtureInfo
|
|
47
45
|
|
|
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)
|
|
46
|
+
from ..models import APIOperation
|
|
53
47
|
|
|
54
48
|
|
|
55
49
|
class SchemathesisFunction(Function):
|
|
@@ -155,7 +149,9 @@ class SchemathesisCase(PyCollector):
|
|
|
155
149
|
name += f"[{error.full_path}]"
|
|
156
150
|
|
|
157
151
|
cls = self._get_class_parent()
|
|
158
|
-
definition: FunctionDefinition =
|
|
152
|
+
definition: FunctionDefinition = FunctionDefinition.from_parent(
|
|
153
|
+
name=self.name, parent=self.parent, callobj=funcobj
|
|
154
|
+
)
|
|
159
155
|
fixturemanager = self.session._fixturemanager
|
|
160
156
|
fixtureinfo = fixturemanager.getfixtureinfo(definition, funcobj, cls)
|
|
161
157
|
|
|
@@ -166,8 +162,7 @@ class SchemathesisCase(PyCollector):
|
|
|
166
162
|
funcobj = partial(funcobj, self.parent.obj)
|
|
167
163
|
|
|
168
164
|
if not metafunc._calls:
|
|
169
|
-
yield
|
|
170
|
-
SchemathesisFunction,
|
|
165
|
+
yield SchemathesisFunction.from_parent(
|
|
171
166
|
name=name,
|
|
172
167
|
parent=self.parent,
|
|
173
168
|
callobj=funcobj,
|
|
@@ -181,10 +176,9 @@ class SchemathesisCase(PyCollector):
|
|
|
181
176
|
fixtureinfo.prune_dependency_tree()
|
|
182
177
|
for callspec in metafunc._calls:
|
|
183
178
|
subname = f"{name}[{callspec.id}]"
|
|
184
|
-
yield
|
|
185
|
-
|
|
179
|
+
yield SchemathesisFunction.from_parent(
|
|
180
|
+
self.parent,
|
|
186
181
|
name=subname,
|
|
187
|
-
parent=self.parent,
|
|
188
182
|
callspec=callspec,
|
|
189
183
|
callobj=funcobj,
|
|
190
184
|
fixtureinfo=fixtureinfo,
|
|
@@ -206,7 +200,7 @@ class SchemathesisCase(PyCollector):
|
|
|
206
200
|
kwargs["_ispytest"] = True
|
|
207
201
|
metafunc = Metafunc(definition, fixtureinfo, self.config, **kwargs)
|
|
208
202
|
methods = []
|
|
209
|
-
if hasattr(module, "pytest_generate_tests"):
|
|
203
|
+
if module is not None and hasattr(module, "pytest_generate_tests"):
|
|
210
204
|
methods.append(module.pytest_generate_tests)
|
|
211
205
|
if hasattr(cls, "pytest_generate_tests"):
|
|
212
206
|
cls = cast(Type, cls)
|
|
@@ -236,7 +230,7 @@ def pytest_pycollect_makeitem(collector: nodes.Collector, name: str, obj: Any) -
|
|
|
236
230
|
"""Switch to a different collector if the test is parametrized marked by schemathesis."""
|
|
237
231
|
outcome = yield
|
|
238
232
|
if is_schemathesis_test(obj):
|
|
239
|
-
outcome.force_result(
|
|
233
|
+
outcome.force_result(SchemathesisCase.from_parent(collector, test_function=obj, name=name))
|
|
240
234
|
else:
|
|
241
235
|
outcome.get_result()
|
|
242
236
|
|
|
@@ -261,7 +255,7 @@ def skip_unnecessary_hypothesis_output() -> Generator:
|
|
|
261
255
|
yield
|
|
262
256
|
|
|
263
257
|
|
|
264
|
-
@hookimpl(
|
|
258
|
+
@hookimpl(wrapper=True)
|
|
265
259
|
def pytest_pyfunc_call(pyfuncitem): # type:ignore
|
|
266
260
|
"""It is possible to have a Hypothesis exception in runtime.
|
|
267
261
|
|
|
@@ -278,10 +272,9 @@ def pytest_pyfunc_call(pyfuncitem): # type:ignore
|
|
|
278
272
|
|
|
279
273
|
__tracebackhide__ = True
|
|
280
274
|
if isinstance(pyfuncitem, SchemathesisFunction):
|
|
281
|
-
with skip_unnecessary_hypothesis_output():
|
|
282
|
-
outcome = yield
|
|
283
275
|
try:
|
|
284
|
-
|
|
276
|
+
with skip_unnecessary_hypothesis_output():
|
|
277
|
+
yield
|
|
285
278
|
except InvalidArgument as exc:
|
|
286
279
|
if "Inconsistent args" in str(exc) and "@example()" in str(exc):
|
|
287
280
|
raise UsageError(GIVEN_AND_EXPLICIT_EXAMPLES_ERROR_MESSAGE) from None
|
|
@@ -316,5 +309,4 @@ def pytest_pyfunc_call(pyfuncitem): # type:ignore
|
|
|
316
309
|
if invalid_headers is not None:
|
|
317
310
|
raise InvalidHeadersExample.from_headers(invalid_headers) from None
|
|
318
311
|
else:
|
|
319
|
-
|
|
320
|
-
outcome.get_result()
|
|
312
|
+
yield
|