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
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass, replace
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
TRUNCATED = "// Output truncated..."
|
|
8
|
+
MAX_PAYLOAD_SIZE = 512
|
|
9
|
+
MAX_LINES = 10
|
|
10
|
+
MAX_WIDTH = 80
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class OutputConfig:
|
|
15
|
+
"""Options for configuring various aspects of Schemathesis output."""
|
|
16
|
+
|
|
17
|
+
truncate: bool = True
|
|
18
|
+
max_payload_size: int = MAX_PAYLOAD_SIZE
|
|
19
|
+
max_lines: int = MAX_LINES
|
|
20
|
+
max_width: int = MAX_WIDTH
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def from_parent(cls, parent: OutputConfig | None = None, **changes: Any) -> OutputConfig:
|
|
24
|
+
parent = parent or OutputConfig()
|
|
25
|
+
return parent.replace(**changes)
|
|
26
|
+
|
|
27
|
+
def replace(self, **changes: Any) -> OutputConfig:
|
|
28
|
+
"""Create a new instance with updated values."""
|
|
29
|
+
return replace(self, **changes)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def truncate_json(data: Any, *, config: OutputConfig | None = None) -> str:
|
|
33
|
+
config = config or OutputConfig()
|
|
34
|
+
# Convert JSON to string with indentation
|
|
35
|
+
indent = 4
|
|
36
|
+
serialized = json.dumps(data, indent=indent)
|
|
37
|
+
if not config.truncate:
|
|
38
|
+
return serialized
|
|
39
|
+
|
|
40
|
+
# Split string by lines
|
|
41
|
+
|
|
42
|
+
lines = [
|
|
43
|
+
line[: config.max_width - 3] + "..." if len(line) > config.max_width else line
|
|
44
|
+
for line in serialized.split("\n")
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
if len(lines) <= config.max_lines:
|
|
48
|
+
return "\n".join(lines)
|
|
49
|
+
|
|
50
|
+
truncated_lines = lines[: config.max_lines - 1]
|
|
51
|
+
indentation = " " * indent
|
|
52
|
+
truncated_lines.append(f"{indentation}{TRUNCATED}")
|
|
53
|
+
truncated_lines.append(lines[-1])
|
|
54
|
+
|
|
55
|
+
return "\n".join(truncated_lines)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def prepare_response_payload(payload: str, *, config: OutputConfig | None = None) -> str:
|
|
59
|
+
if payload.endswith("\r\n"):
|
|
60
|
+
payload = payload[:-2]
|
|
61
|
+
elif payload.endswith("\n"):
|
|
62
|
+
payload = payload[:-1]
|
|
63
|
+
config = config or OutputConfig()
|
|
64
|
+
if not config.truncate:
|
|
65
|
+
return payload
|
|
66
|
+
if len(payload) > config.max_payload_size:
|
|
67
|
+
payload = payload[: config.max_payload_size] + f" {TRUNCATED}"
|
|
68
|
+
return payload
|
schemathesis/internal/result.py
CHANGED
schemathesis/lazy.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
from dataclasses import dataclass, field
|
|
3
4
|
from inspect import signature
|
|
4
5
|
from typing import Any, Callable, Generator
|
|
@@ -13,13 +14,14 @@ from pyrate_limiter import Limiter
|
|
|
13
14
|
from pytest_subtests import SubTests, nullcontext
|
|
14
15
|
|
|
15
16
|
from ._compat import MultipleFailures, get_interesting_origin
|
|
16
|
-
from ._override import check_no_override_mark,
|
|
17
|
+
from ._override import CaseOverride, check_no_override_mark, get_override_from_mark, set_override_mark
|
|
17
18
|
from .auths import AuthStorage
|
|
18
19
|
from .code_samples import CodeSampleStyle
|
|
19
20
|
from .constants import FLAKY_FAILURE_MESSAGE, NOT_SET
|
|
20
|
-
from .generation import DataGenerationMethodInput, GenerationConfig
|
|
21
21
|
from .exceptions import CheckFailed, OperationSchemaError, SkipTest, get_grouped_exception
|
|
22
|
+
from .generation import DataGenerationMethodInput, GenerationConfig
|
|
22
23
|
from .hooks import HookDispatcher, HookScope
|
|
24
|
+
from .internal.output import OutputConfig
|
|
23
25
|
from .internal.result import Ok
|
|
24
26
|
from .models import APIOperation
|
|
25
27
|
from .schemas import BaseSchema
|
|
@@ -51,6 +53,7 @@ class LazySchema:
|
|
|
51
53
|
skip_deprecated_operations: bool = False
|
|
52
54
|
data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET
|
|
53
55
|
generation_config: GenerationConfig | NotSet = NOT_SET
|
|
56
|
+
output_config: OutputConfig | NotSet = NOT_SET
|
|
54
57
|
code_sample_style: CodeSampleStyle = CodeSampleStyle.default()
|
|
55
58
|
rate_limiter: Limiter | None = None
|
|
56
59
|
sanitize_output: bool = True
|
|
@@ -68,6 +71,7 @@ class LazySchema:
|
|
|
68
71
|
skip_deprecated_operations: bool | NotSet = NOT_SET,
|
|
69
72
|
data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET,
|
|
70
73
|
generation_config: GenerationConfig | NotSet = NOT_SET,
|
|
74
|
+
output_config: OutputConfig | NotSet = NOT_SET,
|
|
71
75
|
code_sample_style: str | NotSet = NOT_SET,
|
|
72
76
|
) -> Callable:
|
|
73
77
|
if method is NOT_SET:
|
|
@@ -82,6 +86,8 @@ class LazySchema:
|
|
|
82
86
|
data_generation_methods = self.data_generation_methods
|
|
83
87
|
if generation_config is NOT_SET:
|
|
84
88
|
generation_config = self.generation_config
|
|
89
|
+
if output_config is NOT_SET:
|
|
90
|
+
output_config = self.output_config
|
|
85
91
|
if isinstance(code_sample_style, str):
|
|
86
92
|
_code_sample_style = CodeSampleStyle.from_str(code_sample_style)
|
|
87
93
|
else:
|
|
@@ -121,6 +127,7 @@ class LazySchema:
|
|
|
121
127
|
skip_deprecated_operations=skip_deprecated_operations,
|
|
122
128
|
data_generation_methods=data_generation_methods,
|
|
123
129
|
generation_config=generation_config,
|
|
130
|
+
output_config=output_config,
|
|
124
131
|
code_sample_style=_code_sample_style,
|
|
125
132
|
app=self.app,
|
|
126
133
|
rate_limiter=self.rate_limiter,
|
|
@@ -325,6 +332,7 @@ def get_schema(
|
|
|
325
332
|
skip_deprecated_operations: bool | NotSet = NOT_SET,
|
|
326
333
|
data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET,
|
|
327
334
|
generation_config: GenerationConfig | NotSet = NOT_SET,
|
|
335
|
+
output_config: OutputConfig | NotSet = NOT_SET,
|
|
328
336
|
code_sample_style: CodeSampleStyle,
|
|
329
337
|
rate_limiter: Limiter | None,
|
|
330
338
|
sanitize_output: bool,
|
|
@@ -347,6 +355,7 @@ def get_schema(
|
|
|
347
355
|
skip_deprecated_operations=skip_deprecated_operations,
|
|
348
356
|
data_generation_methods=data_generation_methods,
|
|
349
357
|
generation_config=generation_config,
|
|
358
|
+
output_config=output_config,
|
|
350
359
|
code_sample_style=code_sample_style,
|
|
351
360
|
rate_limiter=rate_limiter,
|
|
352
361
|
sanitize_output=sanitize_output,
|
schemathesis/loaders.py
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import re
|
|
3
4
|
import sys
|
|
4
5
|
from functools import lru_cache
|
|
5
|
-
from typing import
|
|
6
|
+
from typing import TYPE_CHECKING, Any, BinaryIO, Callable, TextIO, TypeVar
|
|
6
7
|
|
|
7
8
|
from .exceptions import SchemaError, SchemaErrorType, extract_requests_exception_details
|
|
8
9
|
|
|
9
10
|
if TYPE_CHECKING:
|
|
10
|
-
from .transports.responses import GenericResponse
|
|
11
11
|
import yaml
|
|
12
12
|
|
|
13
|
+
from .transports.responses import GenericResponse
|
|
14
|
+
|
|
13
15
|
R = TypeVar("R", bound="GenericResponse")
|
|
14
16
|
|
|
15
17
|
|
schemathesis/models.py
CHANGED
|
@@ -42,19 +42,20 @@ from .exceptions import (
|
|
|
42
42
|
OperationSchemaError,
|
|
43
43
|
SerializationNotPossible,
|
|
44
44
|
SkipTest,
|
|
45
|
+
UsageError,
|
|
45
46
|
deduplicate_failed_checks,
|
|
46
47
|
get_grouped_exception,
|
|
47
48
|
maybe_set_assertion_message,
|
|
48
|
-
prepare_response_payload,
|
|
49
49
|
)
|
|
50
50
|
from .generation import DataGenerationMethod, GenerationConfig, generate_random_case_id
|
|
51
51
|
from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, dispatch
|
|
52
52
|
from .internal.copy import fast_deepcopy
|
|
53
53
|
from .internal.deprecation import deprecated_function, deprecated_property
|
|
54
|
+
from .internal.output import prepare_response_payload
|
|
54
55
|
from .parameters import Parameter, ParameterSet, PayloadAlternatives
|
|
55
56
|
from .sanitization import sanitize_request, sanitize_response
|
|
56
57
|
from .serializers import Serializer
|
|
57
|
-
from .transports import ASGITransport, RequestsTransport, WSGITransport,
|
|
58
|
+
from .transports import ASGITransport, RequestsTransport, WSGITransport, deserialize_payload, serialize_payload
|
|
58
59
|
from .types import Body, Cookies, FormData, Headers, NotSet, PathParameters, Query
|
|
59
60
|
|
|
60
61
|
if TYPE_CHECKING:
|
|
@@ -271,13 +272,14 @@ class Case:
|
|
|
271
272
|
final_headers.setdefault(SCHEMATHESIS_TEST_CASE_HEADER, self.id)
|
|
272
273
|
return final_headers
|
|
273
274
|
|
|
274
|
-
def _get_serializer(self) -> Serializer | None:
|
|
275
|
+
def _get_serializer(self, media_type: str | None = None) -> Serializer | None:
|
|
275
276
|
"""Get a serializer for the payload, if there is any."""
|
|
276
|
-
|
|
277
|
-
|
|
277
|
+
input_media_type = media_type or self.media_type
|
|
278
|
+
if input_media_type is not None:
|
|
279
|
+
media_type = serializers.get_first_matching_media_type(input_media_type)
|
|
278
280
|
if media_type is None:
|
|
279
281
|
# This media type is set manually. Otherwise, it should have been rejected during the data generation
|
|
280
|
-
raise SerializationNotPossible.for_media_type(
|
|
282
|
+
raise SerializationNotPossible.for_media_type(input_media_type)
|
|
281
283
|
# SAFETY: It is safe to assume that serializer will be found, because `media_type` returned above
|
|
282
284
|
# is registered. This intentionally ignores cases with concurrent serializers registry modification.
|
|
283
285
|
cls = cast(Type[serializers.Serializer], serializers.get(media_type))
|
|
@@ -418,7 +420,7 @@ class Case:
|
|
|
418
420
|
if not payload:
|
|
419
421
|
formatted += "\n\n <EMPTY>"
|
|
420
422
|
else:
|
|
421
|
-
payload = prepare_response_payload(payload)
|
|
423
|
+
payload = prepare_response_payload(payload, config=self.operation.schema.output_config)
|
|
422
424
|
payload = textwrap.indent(f"\n`{payload}`", prefix=" ")
|
|
423
425
|
formatted += f"\n{payload}"
|
|
424
426
|
code_sample_style = (
|
|
@@ -694,6 +696,19 @@ class APIOperation(Generic[P, C]):
|
|
|
694
696
|
def get_request_payload_content_types(self) -> list[str]:
|
|
695
697
|
return self.schema.get_request_payload_content_types(self)
|
|
696
698
|
|
|
699
|
+
def _get_default_media_type(self) -> str:
|
|
700
|
+
# If the user wants to send payload, then there should be a media type, otherwise the payload is ignored
|
|
701
|
+
media_types = self.get_request_payload_content_types()
|
|
702
|
+
if len(media_types) == 1:
|
|
703
|
+
# The only available option
|
|
704
|
+
return media_types[0]
|
|
705
|
+
media_types_repr = ", ".join(media_types)
|
|
706
|
+
raise UsageError(
|
|
707
|
+
"Can not detect appropriate media type. "
|
|
708
|
+
"You can either specify one of the defined media types "
|
|
709
|
+
f"or pass any other media type available for serialization. Defined media types: {media_types_repr}"
|
|
710
|
+
)
|
|
711
|
+
|
|
697
712
|
def partial_deepcopy(self) -> APIOperation:
|
|
698
713
|
return self.__class__(
|
|
699
714
|
path=self.path, # string, immutable
|
schemathesis/parameters.py
CHANGED
schemathesis/runner/__init__.py
CHANGED
|
@@ -28,10 +28,10 @@ if TYPE_CHECKING:
|
|
|
28
28
|
|
|
29
29
|
from ..models import CheckFunction
|
|
30
30
|
from ..schemas import BaseSchema
|
|
31
|
+
from ..service.client import ServiceClient
|
|
31
32
|
from ..stateful import Stateful
|
|
32
33
|
from . import events
|
|
33
34
|
from .impl import BaseRunner
|
|
34
|
-
from ..service.client import ServiceClient
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
@deprecated_function(removed_in="4.0", replacement="schemathesis.runner.from_schema")
|
schemathesis/runner/events.py
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import enum
|
|
3
4
|
import threading
|
|
4
5
|
import time
|
|
5
6
|
from dataclasses import asdict, dataclass, field
|
|
6
|
-
from typing import
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
7
8
|
|
|
9
|
+
from ..exceptions import RuntimeErrorType, SchemaError, SchemaErrorType, format_exception
|
|
10
|
+
from ..generation import DataGenerationMethod
|
|
8
11
|
from ..internal.datetime import current_datetime
|
|
9
12
|
from ..internal.result import Result
|
|
10
|
-
from ..generation import DataGenerationMethod
|
|
11
|
-
from ..exceptions import SchemaError, SchemaErrorType, format_exception, RuntimeErrorType
|
|
12
13
|
from .serialization import SerializedError, SerializedTestResult
|
|
13
14
|
|
|
14
|
-
|
|
15
15
|
if TYPE_CHECKING:
|
|
16
16
|
from ..models import APIOperation, Status, TestResult, TestResultSet
|
|
17
17
|
from ..schemas import BaseSchema
|
|
18
18
|
from ..service.models import AnalysisResult
|
|
19
|
+
from ..stateful import events
|
|
19
20
|
from . import probes
|
|
20
21
|
|
|
21
22
|
|
|
@@ -287,6 +288,23 @@ class InternalError(ExecutionEvent):
|
|
|
287
288
|
)
|
|
288
289
|
|
|
289
290
|
|
|
291
|
+
@dataclass
|
|
292
|
+
class StatefulEvent(ExecutionEvent):
|
|
293
|
+
"""Represents an event originating from the state machine runner."""
|
|
294
|
+
|
|
295
|
+
data: events.StatefulEvent
|
|
296
|
+
|
|
297
|
+
__slots__ = ("data",)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@dataclass
|
|
301
|
+
class AfterStatefulExecution(ExecutionEvent):
|
|
302
|
+
"""Happens after the stateful test run."""
|
|
303
|
+
|
|
304
|
+
status: Status
|
|
305
|
+
result: SerializedTestResult
|
|
306
|
+
|
|
307
|
+
|
|
290
308
|
@dataclass
|
|
291
309
|
class Finished(ExecutionEvent):
|
|
292
310
|
"""The final event of the run.
|
schemathesis/runner/impl/core.py
CHANGED
|
@@ -21,9 +21,7 @@ from jsonschema.exceptions import SchemaError as JsonSchemaError
|
|
|
21
21
|
from jsonschema.exceptions import ValidationError
|
|
22
22
|
from requests.auth import HTTPDigestAuth, _basic_auth_str
|
|
23
23
|
|
|
24
|
-
from
|
|
25
|
-
|
|
26
|
-
from ... import failures, hooks
|
|
24
|
+
from ... import experimental, failures, hooks
|
|
27
25
|
from ..._compat import MultipleFailures
|
|
28
26
|
from ..._hypothesis import (
|
|
29
27
|
get_invalid_example_headers_mark,
|
|
@@ -65,7 +63,10 @@ from ...service import extensions
|
|
|
65
63
|
from ...service.models import AnalysisResult, AnalysisSuccess
|
|
66
64
|
from ...specs.openapi import formats
|
|
67
65
|
from ...stateful import Feedback, Stateful
|
|
66
|
+
from ...stateful import events as stateful_events
|
|
67
|
+
from ...stateful import runner as stateful_runner
|
|
68
68
|
from ...targets import Target, TargetContext
|
|
69
|
+
from ...transports import RequestsTransport, prepare_timeout
|
|
69
70
|
from ...types import RawAuth, RequestCert
|
|
70
71
|
from ...utils import capture_hypothesis_output
|
|
71
72
|
from .. import probes
|
|
@@ -191,7 +192,9 @@ class BaseRunner:
|
|
|
191
192
|
return
|
|
192
193
|
|
|
193
194
|
try:
|
|
194
|
-
|
|
195
|
+
if not experimental.STATEFUL_ONLY.is_enabled:
|
|
196
|
+
yield from self._execute(results, stop_event)
|
|
197
|
+
yield from self._run_stateful_tests(results)
|
|
195
198
|
except KeyboardInterrupt:
|
|
196
199
|
yield events.Interrupted()
|
|
197
200
|
|
|
@@ -211,6 +214,40 @@ class BaseRunner:
|
|
|
211
214
|
) -> Generator[events.ExecutionEvent, None, None]:
|
|
212
215
|
raise NotImplementedError
|
|
213
216
|
|
|
217
|
+
def _run_stateful_tests(self, results: TestResultSet) -> Generator[events.ExecutionEvent, None, None]:
|
|
218
|
+
# Run new-style stateful tests
|
|
219
|
+
if self.stateful is not None and experimental.STATEFUL_TEST_RUNNER.is_enabled and self.schema.links_count > 0:
|
|
220
|
+
result = TestResult(
|
|
221
|
+
method="",
|
|
222
|
+
path="",
|
|
223
|
+
verbose_name="Stateful tests",
|
|
224
|
+
data_generation_method=self.schema.data_generation_methods,
|
|
225
|
+
)
|
|
226
|
+
config = stateful_runner.StatefulTestRunnerConfig(
|
|
227
|
+
checks=tuple(self.checks),
|
|
228
|
+
headers=self.headers or {},
|
|
229
|
+
hypothesis_settings=self.hypothesis_settings,
|
|
230
|
+
exit_first=self.exit_first,
|
|
231
|
+
request_timeout=self.request_timeout,
|
|
232
|
+
)
|
|
233
|
+
state_machine = self.schema.as_state_machine()
|
|
234
|
+
runner = state_machine.runner(config=config)
|
|
235
|
+
status = Status.success
|
|
236
|
+
for stateful_event in runner.execute():
|
|
237
|
+
if isinstance(stateful_event, stateful_events.SuiteFinished):
|
|
238
|
+
if stateful_event.failures and status != Status.error:
|
|
239
|
+
status = Status.failure
|
|
240
|
+
for failure in stateful_event.failures:
|
|
241
|
+
result.checks.append(failure)
|
|
242
|
+
elif isinstance(stateful_event, stateful_events.Errored):
|
|
243
|
+
status = Status.error
|
|
244
|
+
yield events.StatefulEvent(data=stateful_event)
|
|
245
|
+
results.append(result)
|
|
246
|
+
yield events.AfterStatefulExecution(
|
|
247
|
+
status=status,
|
|
248
|
+
result=SerializedTestResult.from_test_result(result),
|
|
249
|
+
)
|
|
250
|
+
|
|
214
251
|
def _run_tests(
|
|
215
252
|
self,
|
|
216
253
|
maker: Callable,
|
|
@@ -246,7 +283,10 @@ class BaseRunner:
|
|
|
246
283
|
):
|
|
247
284
|
if isinstance(result, Ok):
|
|
248
285
|
operation, test = result.ok()
|
|
249
|
-
|
|
286
|
+
if self.stateful is not None and not experimental.STATEFUL_TEST_RUNNER.is_enabled:
|
|
287
|
+
feedback = Feedback(self.stateful, operation)
|
|
288
|
+
else:
|
|
289
|
+
feedback = None
|
|
250
290
|
# Track whether `BeforeExecution` was already emitted.
|
|
251
291
|
# Schema error may happen before / after `BeforeExecution`, but it should be emitted only once
|
|
252
292
|
# and the `AfterExecution` event should have the same correlation id as previous `BeforeExecution`
|
|
@@ -268,17 +308,18 @@ class BaseRunner:
|
|
|
268
308
|
if isinstance(event, events.Interrupted):
|
|
269
309
|
return
|
|
270
310
|
# Additional tests, generated via the `feedback` instance
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
311
|
+
if feedback is not None:
|
|
312
|
+
yield from self._run_tests(
|
|
313
|
+
feedback.get_stateful_tests,
|
|
314
|
+
template,
|
|
315
|
+
settings=settings,
|
|
316
|
+
generation_config=generation_config,
|
|
317
|
+
seed=seed,
|
|
318
|
+
recursion_level=recursion_level + 1,
|
|
319
|
+
results=results,
|
|
320
|
+
headers=headers,
|
|
321
|
+
**kwargs,
|
|
322
|
+
)
|
|
282
323
|
except OperationSchemaError as exc:
|
|
283
324
|
yield from handle_schema_error(
|
|
284
325
|
exc,
|
|
@@ -788,7 +829,7 @@ def network_test(
|
|
|
788
829
|
request_cert: RequestCert | None,
|
|
789
830
|
store_interactions: bool,
|
|
790
831
|
headers: dict[str, Any] | None,
|
|
791
|
-
feedback: Feedback,
|
|
832
|
+
feedback: Feedback | None,
|
|
792
833
|
max_response_time: int | None,
|
|
793
834
|
data_generation_methods: list[DataGenerationMethod],
|
|
794
835
|
dry_run: bool,
|
|
@@ -830,7 +871,7 @@ def _network_test(
|
|
|
830
871
|
timeout: float | None,
|
|
831
872
|
store_interactions: bool,
|
|
832
873
|
headers: dict[str, Any] | None,
|
|
833
|
-
feedback: Feedback,
|
|
874
|
+
feedback: Feedback | None,
|
|
834
875
|
request_tls_verify: bool,
|
|
835
876
|
request_proxy: str | None,
|
|
836
877
|
request_cert: RequestCert | None,
|
|
@@ -877,7 +918,8 @@ def _network_test(
|
|
|
877
918
|
status = Status.failure
|
|
878
919
|
raise
|
|
879
920
|
finally:
|
|
880
|
-
feedback
|
|
921
|
+
if feedback is not None:
|
|
922
|
+
feedback.add_test_case(case, response)
|
|
881
923
|
if store_interactions:
|
|
882
924
|
result.store_requests_response(case, response, status, check_results)
|
|
883
925
|
return response
|
|
@@ -891,14 +933,6 @@ def get_session(auth: HTTPDigestAuth | RawAuth | None = None) -> Generator[reque
|
|
|
891
933
|
yield session
|
|
892
934
|
|
|
893
935
|
|
|
894
|
-
def prepare_timeout(timeout: int | None) -> float | None:
|
|
895
|
-
"""Request timeout is in milliseconds, but `requests` uses seconds."""
|
|
896
|
-
output: int | float | None = timeout
|
|
897
|
-
if timeout is not None:
|
|
898
|
-
output = timeout / 1000
|
|
899
|
-
return output
|
|
900
|
-
|
|
901
|
-
|
|
902
936
|
def wsgi_test(
|
|
903
937
|
case: Case,
|
|
904
938
|
checks: Iterable[CheckFunction],
|
|
@@ -908,7 +942,7 @@ def wsgi_test(
|
|
|
908
942
|
auth_type: str | None,
|
|
909
943
|
headers: dict[str, Any] | None,
|
|
910
944
|
store_interactions: bool,
|
|
911
|
-
feedback: Feedback,
|
|
945
|
+
feedback: Feedback | None,
|
|
912
946
|
max_response_time: int | None,
|
|
913
947
|
data_generation_methods: list[DataGenerationMethod],
|
|
914
948
|
dry_run: bool,
|
|
@@ -939,7 +973,7 @@ def _wsgi_test(
|
|
|
939
973
|
result: TestResult,
|
|
940
974
|
headers: dict[str, Any],
|
|
941
975
|
store_interactions: bool,
|
|
942
|
-
feedback: Feedback,
|
|
976
|
+
feedback: Feedback | None,
|
|
943
977
|
max_response_time: int | None,
|
|
944
978
|
) -> WSGIResponse:
|
|
945
979
|
from ...transports.responses import WSGIResponse
|
|
@@ -968,7 +1002,8 @@ def _wsgi_test(
|
|
|
968
1002
|
status = Status.failure
|
|
969
1003
|
raise
|
|
970
1004
|
finally:
|
|
971
|
-
feedback
|
|
1005
|
+
if feedback is not None:
|
|
1006
|
+
feedback.add_test_case(case, response)
|
|
972
1007
|
if store_interactions:
|
|
973
1008
|
result.store_wsgi_response(case, response, headers, response.elapsed.total_seconds(), status, check_results)
|
|
974
1009
|
return response
|
|
@@ -1001,7 +1036,7 @@ def asgi_test(
|
|
|
1001
1036
|
result: TestResult,
|
|
1002
1037
|
store_interactions: bool,
|
|
1003
1038
|
headers: dict[str, Any] | None,
|
|
1004
|
-
feedback: Feedback,
|
|
1039
|
+
feedback: Feedback | None,
|
|
1005
1040
|
max_response_time: int | None,
|
|
1006
1041
|
data_generation_methods: list[DataGenerationMethod],
|
|
1007
1042
|
dry_run: bool,
|
|
@@ -1034,7 +1069,7 @@ def _asgi_test(
|
|
|
1034
1069
|
result: TestResult,
|
|
1035
1070
|
store_interactions: bool,
|
|
1036
1071
|
headers: dict[str, Any] | None,
|
|
1037
|
-
feedback: Feedback,
|
|
1072
|
+
feedback: Feedback | None,
|
|
1038
1073
|
max_response_time: int | None,
|
|
1039
1074
|
) -> requests.Response:
|
|
1040
1075
|
hook_context = HookContext(operation=case.operation)
|
|
@@ -1059,7 +1094,8 @@ def _asgi_test(
|
|
|
1059
1094
|
status = Status.failure
|
|
1060
1095
|
raise
|
|
1061
1096
|
finally:
|
|
1062
|
-
feedback
|
|
1097
|
+
if feedback is not None:
|
|
1098
|
+
feedback.add_test_case(case, response)
|
|
1063
1099
|
if store_interactions:
|
|
1064
1100
|
result.store_requests_response(case, response, status, check_results)
|
|
1065
1101
|
return response
|
schemathesis/runner/impl/solo.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import threading
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from typing import Generator
|
|
5
6
|
|
|
6
7
|
from ...models import TestResultSet
|
|
7
|
-
from ...types import RequestCert
|
|
8
8
|
from ...transports.auth import get_requests_auth
|
|
9
|
+
from ...types import RequestCert
|
|
9
10
|
from .. import events
|
|
10
11
|
from .core import BaseRunner, asgi_test, get_session, network_test, wsgi_test
|
|
11
12
|
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import ctypes
|
|
3
4
|
import queue
|
|
4
5
|
import threading
|
|
5
6
|
import time
|
|
7
|
+
import warnings
|
|
6
8
|
from dataclasses import dataclass
|
|
7
9
|
from queue import Queue
|
|
8
10
|
from typing import Any, Callable, Generator, Iterable, cast
|
|
9
11
|
|
|
10
12
|
import hypothesis
|
|
13
|
+
from hypothesis.errors import HypothesisWarning
|
|
11
14
|
|
|
12
15
|
from ..._hypothesis import create_test
|
|
13
16
|
from ...generation import DataGenerationMethod, GenerationConfig
|
|
@@ -39,6 +42,7 @@ def _run_task(
|
|
|
39
42
|
headers: dict[str, Any] | None = None,
|
|
40
43
|
**kwargs: Any,
|
|
41
44
|
) -> None:
|
|
45
|
+
warnings.filterwarnings("ignore", message="The recursion limit will not be reset", category=HypothesisWarning)
|
|
42
46
|
as_strategy_kwargs = {}
|
|
43
47
|
if headers is not None:
|
|
44
48
|
as_strategy_kwargs["headers"] = {key: value for key, value in headers.items() if key.lower() != "user-agent"}
|
schemathesis/runner/probes.py
CHANGED
|
@@ -28,7 +28,7 @@ from ..exceptions import (
|
|
|
28
28
|
make_unique_by_key,
|
|
29
29
|
)
|
|
30
30
|
from ..models import Case, Check, Interaction, Request, Response, Status, TestResult
|
|
31
|
-
from ..transports import
|
|
31
|
+
from ..transports import deserialize_payload, serialize_payload
|
|
32
32
|
|
|
33
33
|
if TYPE_CHECKING:
|
|
34
34
|
import hypothesis.errors
|
schemathesis/sanitization.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import threading
|
|
3
4
|
from collections.abc import MutableMapping, MutableSequence
|
|
4
5
|
from dataclasses import dataclass, replace
|
|
@@ -9,6 +10,7 @@ from .constants import NOT_SET
|
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
11
12
|
from requests import PreparedRequest
|
|
13
|
+
|
|
12
14
|
from .models import Case, CaseSource, Request
|
|
13
15
|
from .runner.serialization import SerializedCase, SerializedCheck, SerializedInteraction
|
|
14
16
|
from .transports.responses import GenericResponse
|
schemathesis/schemas.py
CHANGED
|
@@ -44,6 +44,7 @@ from .generation import (
|
|
|
44
44
|
GenerationConfig,
|
|
45
45
|
)
|
|
46
46
|
from .hooks import HookContext, HookDispatcher, HookScope, dispatch
|
|
47
|
+
from .internal.output import OutputConfig
|
|
47
48
|
from .internal.result import Ok, Result
|
|
48
49
|
from .models import APIOperation, Case
|
|
49
50
|
from .stateful import Stateful, StatefulTest
|
|
@@ -94,6 +95,7 @@ class BaseSchema(Mapping):
|
|
|
94
95
|
default_factory=lambda: list(DEFAULT_DATA_GENERATION_METHODS)
|
|
95
96
|
)
|
|
96
97
|
generation_config: GenerationConfig = field(default_factory=GenerationConfig)
|
|
98
|
+
output_config: OutputConfig = field(default_factory=OutputConfig)
|
|
97
99
|
code_sample_style: CodeSampleStyle = CodeSampleStyle.default()
|
|
98
100
|
rate_limiter: Limiter | None = None
|
|
99
101
|
sanitize_output: bool = True
|
|
@@ -286,6 +288,7 @@ class BaseSchema(Mapping):
|
|
|
286
288
|
skip_deprecated_operations: bool | NotSet = NOT_SET,
|
|
287
289
|
data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET,
|
|
288
290
|
generation_config: GenerationConfig | NotSet = NOT_SET,
|
|
291
|
+
output_config: OutputConfig | NotSet = NOT_SET,
|
|
289
292
|
code_sample_style: CodeSampleStyle | NotSet = NOT_SET,
|
|
290
293
|
rate_limiter: Limiter | None = NOT_SET,
|
|
291
294
|
sanitize_output: bool | NotSet | None = NOT_SET,
|
|
@@ -314,6 +317,8 @@ class BaseSchema(Mapping):
|
|
|
314
317
|
data_generation_methods = self.data_generation_methods
|
|
315
318
|
if generation_config is NOT_SET:
|
|
316
319
|
generation_config = self.generation_config
|
|
320
|
+
if output_config is NOT_SET:
|
|
321
|
+
output_config = self.output_config
|
|
317
322
|
if code_sample_style is NOT_SET:
|
|
318
323
|
code_sample_style = self.code_sample_style
|
|
319
324
|
if rate_limiter is NOT_SET:
|
|
@@ -337,6 +342,7 @@ class BaseSchema(Mapping):
|
|
|
337
342
|
skip_deprecated_operations=skip_deprecated_operations, # type: ignore
|
|
338
343
|
data_generation_methods=data_generation_methods, # type: ignore
|
|
339
344
|
generation_config=generation_config, # type: ignore
|
|
345
|
+
output_config=output_config, # type: ignore
|
|
340
346
|
code_sample_style=code_sample_style, # type: ignore
|
|
341
347
|
rate_limiter=rate_limiter, # type: ignore
|
|
342
348
|
sanitize_output=sanitize_output, # type: ignore
|
|
@@ -397,10 +403,7 @@ class BaseSchema(Mapping):
|
|
|
397
403
|
raise NotImplementedError
|
|
398
404
|
|
|
399
405
|
def as_state_machine(self) -> type[APIStateMachine]:
|
|
400
|
-
"""Create a state machine class.
|
|
401
|
-
|
|
402
|
-
Use it for stateful testing.
|
|
403
|
-
"""
|
|
406
|
+
"""Create a state machine class."""
|
|
404
407
|
raise NotImplementedError
|
|
405
408
|
|
|
406
409
|
def get_links(self, operation: APIOperation) -> dict[str, dict[str, Any]]:
|