schemathesis 3.35.4__py3-none-any.whl → 3.35.5__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 +5 -5
- schemathesis/_hypothesis.py +12 -6
- schemathesis/_override.py +4 -4
- schemathesis/auths.py +1 -1
- schemathesis/cli/__init__.py +19 -13
- schemathesis/cli/callbacks.py +6 -4
- schemathesis/cli/cassettes.py +67 -41
- schemathesis/cli/context.py +7 -6
- schemathesis/cli/junitxml.py +1 -1
- schemathesis/cli/options.py +7 -4
- schemathesis/cli/output/default.py +5 -5
- schemathesis/cli/reporting.py +4 -2
- schemathesis/code_samples.py +4 -3
- schemathesis/exceptions.py +4 -3
- schemathesis/extra/_flask.py +4 -1
- schemathesis/extra/pytest_plugin.py +6 -3
- schemathesis/failures.py +2 -1
- schemathesis/filters.py +2 -2
- schemathesis/generation/__init__.py +2 -2
- schemathesis/generation/_hypothesis.py +1 -1
- schemathesis/generation/coverage.py +5 -5
- schemathesis/graphql.py +0 -1
- schemathesis/hooks.py +3 -3
- schemathesis/lazy.py +10 -7
- schemathesis/loaders.py +3 -3
- schemathesis/models.py +39 -15
- schemathesis/runner/__init__.py +5 -5
- schemathesis/runner/events.py +1 -1
- schemathesis/runner/impl/context.py +58 -0
- schemathesis/runner/impl/core.py +54 -61
- schemathesis/runner/impl/solo.py +17 -20
- schemathesis/runner/impl/threadpool.py +65 -71
- schemathesis/runner/serialization.py +4 -3
- schemathesis/sanitization.py +2 -1
- schemathesis/schemas.py +18 -20
- schemathesis/serializers.py +2 -0
- schemathesis/service/client.py +1 -1
- schemathesis/service/events.py +4 -1
- schemathesis/service/extensions.py +2 -2
- schemathesis/service/hosts.py +4 -2
- schemathesis/service/models.py +3 -3
- schemathesis/service/report.py +3 -3
- schemathesis/service/serialization.py +4 -2
- schemathesis/specs/graphql/loaders.py +4 -3
- schemathesis/specs/graphql/schemas.py +4 -3
- schemathesis/specs/openapi/definitions.py +1 -5
- schemathesis/specs/openapi/examples.py +92 -2
- schemathesis/specs/openapi/expressions/__init__.py +7 -0
- schemathesis/specs/openapi/expressions/extractors.py +4 -1
- schemathesis/specs/openapi/expressions/nodes.py +5 -3
- schemathesis/specs/openapi/links.py +4 -4
- schemathesis/specs/openapi/loaders.py +5 -4
- schemathesis/specs/openapi/negative/__init__.py +5 -3
- schemathesis/specs/openapi/negative/mutations.py +5 -4
- schemathesis/specs/openapi/parameters.py +4 -2
- schemathesis/specs/openapi/schemas.py +9 -10
- schemathesis/specs/openapi/security.py +6 -4
- schemathesis/specs/openapi/stateful/__init__.py +2 -2
- schemathesis/specs/openapi/stateful/statistic.py +3 -3
- schemathesis/specs/openapi/stateful/types.py +3 -2
- schemathesis/stateful/__init__.py +3 -3
- schemathesis/stateful/config.py +1 -1
- schemathesis/stateful/context.py +3 -3
- schemathesis/stateful/events.py +3 -3
- schemathesis/stateful/runner.py +5 -4
- schemathesis/stateful/sink.py +1 -1
- schemathesis/stateful/state_machine.py +5 -5
- schemathesis/stateful/statistic.py +3 -1
- schemathesis/stateful/validation.py +1 -1
- schemathesis/transports/__init__.py +2 -2
- schemathesis/transports/asgi.py +7 -0
- schemathesis/transports/auth.py +2 -1
- schemathesis/transports/content_types.py +1 -1
- schemathesis/transports/responses.py +2 -1
- schemathesis/utils.py +4 -2
- {schemathesis-3.35.4.dist-info → schemathesis-3.35.5.dist-info}/METADATA +1 -1
- schemathesis-3.35.5.dist-info/RECORD +156 -0
- schemathesis-3.35.4.dist-info/RECORD +0 -154
- {schemathesis-3.35.4.dist-info → schemathesis-3.35.5.dist-info}/WHEEL +0 -0
- {schemathesis-3.35.4.dist-info → schemathesis-3.35.5.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.35.4.dist-info → schemathesis-3.35.5.dist-info}/licenses/LICENSE +0 -0
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."""
|
|
@@ -3,12 +3,11 @@ 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, cast
|
|
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
11
|
from _pytest.python import Class, Function, FunctionDefinition, Metafunc, Module, PyCollector
|
|
13
12
|
from hypothesis import reporting
|
|
14
13
|
from hypothesis.errors import InvalidArgument, Unsatisfiable
|
|
@@ -30,7 +29,6 @@ from ..exceptions import (
|
|
|
30
29
|
UsageError,
|
|
31
30
|
)
|
|
32
31
|
from ..internal.result import Ok, Result
|
|
33
|
-
from ..models import APIOperation
|
|
34
32
|
from ..utils import (
|
|
35
33
|
PARAMETRIZE_MARKER,
|
|
36
34
|
fail_on_no_matches,
|
|
@@ -42,6 +40,11 @@ from ..utils import (
|
|
|
42
40
|
validate_given_args,
|
|
43
41
|
)
|
|
44
42
|
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from _pytest.fixtures import FuncFixtureInfo
|
|
45
|
+
|
|
46
|
+
from ..models import APIOperation
|
|
47
|
+
|
|
45
48
|
|
|
46
49
|
class SchemathesisFunction(Function):
|
|
47
50
|
def __init__(
|
schemathesis/failures.py
CHANGED
|
@@ -2,12 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import textwrap
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
-
from json import JSONDecodeError
|
|
6
5
|
from typing import TYPE_CHECKING, Any
|
|
7
6
|
|
|
8
7
|
from schemathesis.internal.output import OutputConfig
|
|
9
8
|
|
|
10
9
|
if TYPE_CHECKING:
|
|
10
|
+
from json import JSONDecodeError
|
|
11
|
+
|
|
11
12
|
from graphql.error import GraphQLFormattedError
|
|
12
13
|
from jsonschema import ValidationError
|
|
13
14
|
|
schemathesis/filters.py
CHANGED
|
@@ -54,7 +54,7 @@ class Matcher:
|
|
|
54
54
|
func = partial(by_value_list, attribute=attribute, expected=expected)
|
|
55
55
|
else:
|
|
56
56
|
func = partial(by_value, attribute=attribute, expected=expected)
|
|
57
|
-
label = f"{attribute}={
|
|
57
|
+
label = f"{attribute}={expected!r}"
|
|
58
58
|
return cls(func, label=label, _hash=hash(label))
|
|
59
59
|
|
|
60
60
|
@classmethod
|
|
@@ -68,7 +68,7 @@ class Matcher:
|
|
|
68
68
|
flags = 0
|
|
69
69
|
regex = re.compile(regex, flags=flags)
|
|
70
70
|
func = partial(by_regex, attribute=attribute, regex=regex)
|
|
71
|
-
label = f"{attribute}_regex={
|
|
71
|
+
label = f"{attribute}_regex={regex!r}"
|
|
72
72
|
return cls(func, label=label, _hash=hash(label))
|
|
73
73
|
|
|
74
74
|
def match(self, ctx: HasAPIOperation) -> bool:
|
|
@@ -4,8 +4,8 @@ import random
|
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
|
-
from ._hypothesis import add_single_example, combine_strategies, get_single_example
|
|
8
|
-
from ._methods import DataGenerationMethod, DataGenerationMethodInput
|
|
7
|
+
from ._hypothesis import add_single_example, combine_strategies, get_single_example
|
|
8
|
+
from ._methods import DataGenerationMethod, DataGenerationMethodInput
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
11
|
from hypothesis.strategies import SearchStrategy
|
|
@@ -4,7 +4,7 @@ import json
|
|
|
4
4
|
from contextlib import contextmanager, suppress
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
from functools import lru_cache
|
|
7
|
-
from typing import Any, Generator,
|
|
7
|
+
from typing import Any, Generator, TypeVar, cast
|
|
8
8
|
|
|
9
9
|
import jsonschema
|
|
10
10
|
from hypothesis import strategies as st
|
|
@@ -140,8 +140,8 @@ def _ignore_unfixable(
|
|
|
140
140
|
*,
|
|
141
141
|
# Cache exception types here as `jsonschema` uses a custom `__getattr__` on the module level
|
|
142
142
|
# and it may cause errors during the interpreter shutdown
|
|
143
|
-
ref_error:
|
|
144
|
-
schema_error:
|
|
143
|
+
ref_error: type[Exception] = jsonschema.RefResolutionError,
|
|
144
|
+
schema_error: type[Exception] = jsonschema.SchemaError,
|
|
145
145
|
) -> Generator:
|
|
146
146
|
try:
|
|
147
147
|
yield
|
|
@@ -172,7 +172,7 @@ def cover_schema_iter(ctx: CoverageContext, schema: dict | bool) -> Generator[Ge
|
|
|
172
172
|
yield from _cover_positive_for_type(ctx, schema, ty)
|
|
173
173
|
if DataGenerationMethod.negative in ctx.data_generation_methods:
|
|
174
174
|
template = None
|
|
175
|
-
seen:
|
|
175
|
+
seen: set[Any | tuple[type, str]] = set()
|
|
176
176
|
for key, value in schema.items():
|
|
177
177
|
with _ignore_unfixable():
|
|
178
178
|
if key == "enum":
|
|
@@ -238,7 +238,7 @@ def _get_properties(schema: dict | bool) -> dict | bool:
|
|
|
238
238
|
if isinstance(schema, dict):
|
|
239
239
|
if "example" in schema:
|
|
240
240
|
return {"const": schema["example"]}
|
|
241
|
-
if
|
|
241
|
+
if schema.get("examples"):
|
|
242
242
|
return {"enum": schema["examples"]}
|
|
243
243
|
if schema.get("type") == "object":
|
|
244
244
|
return _get_template_schema(schema, "object")
|
schemathesis/graphql.py
CHANGED
schemathesis/hooks.py
CHANGED
|
@@ -6,11 +6,10 @@ from copy import deepcopy
|
|
|
6
6
|
from dataclasses import dataclass, field
|
|
7
7
|
from enum import Enum, unique
|
|
8
8
|
from functools import partial
|
|
9
|
-
from typing import TYPE_CHECKING, Any, Callable, ClassVar,
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Callable, ClassVar, cast
|
|
10
10
|
|
|
11
11
|
from .filters import FilterSet, attach_filter_chain
|
|
12
12
|
from .internal.deprecation import deprecated_property
|
|
13
|
-
from .types import GenericTest
|
|
14
13
|
|
|
15
14
|
if TYPE_CHECKING:
|
|
16
15
|
from hypothesis import strategies as st
|
|
@@ -18,6 +17,7 @@ if TYPE_CHECKING:
|
|
|
18
17
|
from .models import APIOperation, Case
|
|
19
18
|
from .schemas import BaseSchema
|
|
20
19
|
from .transports.responses import GenericResponse
|
|
20
|
+
from .types import GenericTest
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
@unique
|
|
@@ -108,7 +108,7 @@ class HookDispatcher:
|
|
|
108
108
|
"""
|
|
109
109
|
|
|
110
110
|
scope: HookScope
|
|
111
|
-
_hooks:
|
|
111
|
+
_hooks: defaultdict[str, list[Callable]] = field(default_factory=lambda: defaultdict(list))
|
|
112
112
|
_specs: ClassVar[dict[str, RegisteredHook]] = {}
|
|
113
113
|
|
|
114
114
|
def __post_init__(self) -> None:
|
schemathesis/lazy.py
CHANGED
|
@@ -2,15 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
from inspect import signature
|
|
5
|
-
from typing import Any, Callable, Generator
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Callable, Generator
|
|
6
6
|
|
|
7
7
|
import pytest
|
|
8
|
-
from _pytest.fixtures import FixtureRequest
|
|
9
8
|
from hypothesis.core import HypothesisHandle
|
|
10
9
|
from hypothesis.errors import Flaky
|
|
11
10
|
from hypothesis.internal.escalation import format_exception, get_trimmed_traceback
|
|
12
11
|
from hypothesis.internal.reflection import impersonate
|
|
13
|
-
from pyrate_limiter import Limiter
|
|
14
12
|
from pytest_subtests import SubTests, nullcontext
|
|
15
13
|
|
|
16
14
|
from ._compat import MultipleFailures, get_interesting_origin
|
|
@@ -20,14 +18,10 @@ from .code_samples import CodeSampleStyle
|
|
|
20
18
|
from .constants import FLAKY_FAILURE_MESSAGE, NOT_SET
|
|
21
19
|
from .exceptions import CheckFailed, OperationSchemaError, SkipTest, get_grouped_exception
|
|
22
20
|
from .filters import FilterSet, FilterValue, MatcherFunc, RegexValue, filter_set_from_components, is_deprecated
|
|
23
|
-
from .generation import DataGenerationMethodInput, GenerationConfig
|
|
24
21
|
from .hooks import HookDispatcher, HookScope
|
|
25
22
|
from .internal.deprecation import warn_filtration_arguments
|
|
26
|
-
from .internal.output import OutputConfig
|
|
27
23
|
from .internal.result import Ok
|
|
28
|
-
from .models import APIOperation
|
|
29
24
|
from .schemas import BaseSchema
|
|
30
|
-
from .types import Filter, GenericTest, NotSet
|
|
31
25
|
from .utils import (
|
|
32
26
|
GivenInput,
|
|
33
27
|
fail_on_no_matches,
|
|
@@ -39,6 +33,15 @@ from .utils import (
|
|
|
39
33
|
validate_given_args,
|
|
40
34
|
)
|
|
41
35
|
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from _pytest.fixtures import FixtureRequest
|
|
38
|
+
from pyrate_limiter import Limiter
|
|
39
|
+
|
|
40
|
+
from .generation import DataGenerationMethodInput, GenerationConfig
|
|
41
|
+
from .internal.output import OutputConfig
|
|
42
|
+
from .models import APIOperation
|
|
43
|
+
from .types import Filter, GenericTest, NotSet
|
|
44
|
+
|
|
42
45
|
|
|
43
46
|
@dataclass
|
|
44
47
|
class LazySchema:
|
schemathesis/loaders.py
CHANGED
|
@@ -51,13 +51,13 @@ def _raise_for_status(response: GenericResponse) -> None:
|
|
|
51
51
|
else:
|
|
52
52
|
type_ = SchemaErrorType.HTTP_CLIENT_ERROR
|
|
53
53
|
else:
|
|
54
|
-
return
|
|
54
|
+
return
|
|
55
55
|
raise SchemaError(message=message, type=type_, url=response.request.url, response=response, extras=[])
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
def load_app(path: str) -> Any:
|
|
59
59
|
"""Import an application from a string."""
|
|
60
|
-
path, name = (re.split(
|
|
60
|
+
path, name = ([*re.split(":(?![\\\\/])", path, maxsplit=1), ""])[:2]
|
|
61
61
|
__import__(path)
|
|
62
62
|
# accessing the module from sys.modules returns a proper module, while `__import__`
|
|
63
63
|
# may return a parent module (system dependent)
|
|
@@ -92,7 +92,7 @@ def get_yaml_loader() -> type[yaml.SafeLoader]:
|
|
|
92
92
|
|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*
|
|
93
93
|
|[-+]?\.(?:inf|Inf|INF)
|
|
94
94
|
|\.(?:nan|NaN|NAN))$""",
|
|
95
|
-
re.
|
|
95
|
+
re.VERBOSE,
|
|
96
96
|
),
|
|
97
97
|
list("-+0123456789."),
|
|
98
98
|
)
|
schemathesis/models.py
CHANGED
|
@@ -9,7 +9,6 @@ from dataclasses import dataclass, field
|
|
|
9
9
|
from enum import Enum
|
|
10
10
|
from functools import lru_cache, partial
|
|
11
11
|
from itertools import chain
|
|
12
|
-
from logging import LogRecord
|
|
13
12
|
from typing import (
|
|
14
13
|
TYPE_CHECKING,
|
|
15
14
|
Any,
|
|
@@ -28,7 +27,6 @@ from urllib.parse import quote, unquote, urljoin, urlsplit, urlunsplit
|
|
|
28
27
|
|
|
29
28
|
from . import serializers
|
|
30
29
|
from ._dependency_versions import IS_WERKZEUG_ABOVE_3
|
|
31
|
-
from .auths import AuthStorage
|
|
32
30
|
from .code_samples import CodeSampleStyle
|
|
33
31
|
from .constants import (
|
|
34
32
|
NOT_SET,
|
|
@@ -38,7 +36,6 @@ from .constants import (
|
|
|
38
36
|
)
|
|
39
37
|
from .exceptions import (
|
|
40
38
|
CheckFailed,
|
|
41
|
-
FailureContext,
|
|
42
39
|
OperationSchemaError,
|
|
43
40
|
SerializationNotPossible,
|
|
44
41
|
SkipTest,
|
|
@@ -54,19 +51,22 @@ from .internal.deprecation import deprecated_function, deprecated_property
|
|
|
54
51
|
from .internal.output import prepare_response_payload
|
|
55
52
|
from .parameters import Parameter, ParameterSet, PayloadAlternatives
|
|
56
53
|
from .sanitization import sanitize_request, sanitize_response
|
|
57
|
-
from .serializers import Serializer
|
|
58
54
|
from .transports import ASGITransport, RequestsTransport, WSGITransport, deserialize_payload, serialize_payload
|
|
59
55
|
from .types import Body, Cookies, FormData, Headers, NotSet, PathParameters, Query
|
|
60
56
|
|
|
61
57
|
if TYPE_CHECKING:
|
|
62
58
|
import unittest
|
|
59
|
+
from logging import LogRecord
|
|
63
60
|
|
|
64
61
|
import requests.auth
|
|
65
62
|
import werkzeug
|
|
66
63
|
from hypothesis import strategies as st
|
|
67
64
|
from requests.structures import CaseInsensitiveDict
|
|
68
65
|
|
|
66
|
+
from .auths import AuthStorage
|
|
67
|
+
from .failures import FailureContext
|
|
69
68
|
from .schemas import BaseSchema
|
|
69
|
+
from .serializers import Serializer
|
|
70
70
|
from .stateful import Stateful, StatefulTest
|
|
71
71
|
from .transports.responses import GenericResponse, WSGIResponse
|
|
72
72
|
|
|
@@ -995,7 +995,7 @@ class Interaction:
|
|
|
995
995
|
"""A single interaction with the target app."""
|
|
996
996
|
|
|
997
997
|
request: Request
|
|
998
|
-
response: Response
|
|
998
|
+
response: Response | None
|
|
999
999
|
checks: list[Check]
|
|
1000
1000
|
status: Status
|
|
1001
1001
|
data_generation_method: DataGenerationMethod
|
|
@@ -1003,10 +1003,28 @@ class Interaction:
|
|
|
1003
1003
|
recorded_at: str = field(default_factory=lambda: datetime.datetime.now(TIMEZONE).isoformat())
|
|
1004
1004
|
|
|
1005
1005
|
@classmethod
|
|
1006
|
-
def from_requests(
|
|
1006
|
+
def from_requests(
|
|
1007
|
+
cls,
|
|
1008
|
+
case: Case,
|
|
1009
|
+
response: requests.Response | None,
|
|
1010
|
+
status: Status,
|
|
1011
|
+
checks: list[Check],
|
|
1012
|
+
headers: dict[str, Any] | None,
|
|
1013
|
+
session: requests.Session | None,
|
|
1014
|
+
) -> Interaction:
|
|
1015
|
+
if response is not None:
|
|
1016
|
+
prepared = response.request
|
|
1017
|
+
request = Request.from_prepared_request(prepared)
|
|
1018
|
+
else:
|
|
1019
|
+
import requests
|
|
1020
|
+
|
|
1021
|
+
if session is None:
|
|
1022
|
+
session = requests.Session()
|
|
1023
|
+
session.headers.update(headers or {})
|
|
1024
|
+
request = Request.from_case(case, session)
|
|
1007
1025
|
return cls(
|
|
1008
|
-
request=
|
|
1009
|
-
response=Response.from_requests(response),
|
|
1026
|
+
request=request,
|
|
1027
|
+
response=Response.from_requests(response) if response is not None else None,
|
|
1010
1028
|
status=status,
|
|
1011
1029
|
checks=checks,
|
|
1012
1030
|
data_generation_method=cast(DataGenerationMethod, case.data_generation_method),
|
|
@@ -1017,9 +1035,9 @@ class Interaction:
|
|
|
1017
1035
|
def from_wsgi(
|
|
1018
1036
|
cls,
|
|
1019
1037
|
case: Case,
|
|
1020
|
-
response: WSGIResponse,
|
|
1038
|
+
response: WSGIResponse | None,
|
|
1021
1039
|
headers: dict[str, Any],
|
|
1022
|
-
elapsed: float,
|
|
1040
|
+
elapsed: float | None,
|
|
1023
1041
|
status: Status,
|
|
1024
1042
|
checks: list[Check],
|
|
1025
1043
|
) -> Interaction:
|
|
@@ -1029,7 +1047,7 @@ class Interaction:
|
|
|
1029
1047
|
session.headers.update(headers)
|
|
1030
1048
|
return cls(
|
|
1031
1049
|
request=Request.from_case(case, session),
|
|
1032
|
-
response=Response.from_wsgi(response, elapsed),
|
|
1050
|
+
response=Response.from_wsgi(response, elapsed) if response is not None and elapsed is not None else None,
|
|
1033
1051
|
status=status,
|
|
1034
1052
|
checks=checks,
|
|
1035
1053
|
data_generation_method=cast(DataGenerationMethod, case.data_generation_method),
|
|
@@ -1119,16 +1137,22 @@ class TestResult:
|
|
|
1119
1137
|
self.errors.append(exception)
|
|
1120
1138
|
|
|
1121
1139
|
def store_requests_response(
|
|
1122
|
-
self,
|
|
1140
|
+
self,
|
|
1141
|
+
case: Case,
|
|
1142
|
+
response: requests.Response | None,
|
|
1143
|
+
status: Status,
|
|
1144
|
+
checks: list[Check],
|
|
1145
|
+
headers: dict[str, Any] | None,
|
|
1146
|
+
session: requests.Session | None,
|
|
1123
1147
|
) -> None:
|
|
1124
|
-
self.interactions.append(Interaction.from_requests(case, response, status, checks))
|
|
1148
|
+
self.interactions.append(Interaction.from_requests(case, response, status, checks, headers, session))
|
|
1125
1149
|
|
|
1126
1150
|
def store_wsgi_response(
|
|
1127
1151
|
self,
|
|
1128
1152
|
case: Case,
|
|
1129
|
-
response: WSGIResponse,
|
|
1153
|
+
response: WSGIResponse | None,
|
|
1130
1154
|
headers: dict[str, Any],
|
|
1131
|
-
elapsed: float,
|
|
1155
|
+
elapsed: float | None,
|
|
1132
1156
|
status: Status,
|
|
1133
1157
|
checks: list[Check],
|
|
1134
1158
|
) -> None:
|
schemathesis/runner/__init__.py
CHANGED
|
@@ -4,7 +4,6 @@ from random import Random
|
|
|
4
4
|
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable
|
|
5
5
|
from urllib.parse import urlparse
|
|
6
6
|
|
|
7
|
-
from .._override import CaseOverride
|
|
8
7
|
from ..constants import (
|
|
9
8
|
DEFAULT_DEADLINE,
|
|
10
9
|
DEFAULT_STATEFUL_RECURSION_LIMIT,
|
|
@@ -22,16 +21,17 @@ from ..targets import DEFAULT_TARGETS, Target
|
|
|
22
21
|
from ..transports import RequestConfig
|
|
23
22
|
from ..transports.auth import get_requests_auth
|
|
24
23
|
from ..types import Filter, NotSet, RawAuth, RequestCert
|
|
24
|
+
from . import events
|
|
25
25
|
from .probes import ProbeConfig
|
|
26
26
|
|
|
27
27
|
if TYPE_CHECKING:
|
|
28
28
|
import hypothesis
|
|
29
29
|
|
|
30
|
+
from .._override import CaseOverride
|
|
30
31
|
from ..models import CheckFunction
|
|
31
32
|
from ..schemas import BaseSchema
|
|
32
33
|
from ..service.client import ServiceClient
|
|
33
34
|
from ..stateful import Stateful
|
|
34
|
-
from . import events
|
|
35
35
|
from .impl import BaseRunner
|
|
36
36
|
|
|
37
37
|
|
|
@@ -357,9 +357,9 @@ def from_schema(
|
|
|
357
357
|
service_client: ServiceClient | None = None,
|
|
358
358
|
) -> BaseRunner:
|
|
359
359
|
import hypothesis
|
|
360
|
-
from starlette.applications import Starlette
|
|
361
360
|
|
|
362
361
|
from ..checks import DEFAULT_CHECKS
|
|
362
|
+
from ..transports.asgi import is_asgi_app
|
|
363
363
|
from .impl import (
|
|
364
364
|
SingleThreadASGIRunner,
|
|
365
365
|
SingleThreadRunner,
|
|
@@ -414,7 +414,7 @@ def from_schema(
|
|
|
414
414
|
probe_config=probe_config,
|
|
415
415
|
service_client=service_client,
|
|
416
416
|
)
|
|
417
|
-
if
|
|
417
|
+
if is_asgi_app(schema.app):
|
|
418
418
|
return ThreadPoolASGIRunner(
|
|
419
419
|
schema=schema,
|
|
420
420
|
checks=checks,
|
|
@@ -490,7 +490,7 @@ def from_schema(
|
|
|
490
490
|
probe_config=probe_config,
|
|
491
491
|
service_client=service_client,
|
|
492
492
|
)
|
|
493
|
-
if
|
|
493
|
+
if is_asgi_app(schema.app):
|
|
494
494
|
return SingleThreadASGIRunner(
|
|
495
495
|
schema=schema,
|
|
496
496
|
checks=checks,
|
schemathesis/runner/events.py
CHANGED
|
@@ -7,12 +7,12 @@ from dataclasses import asdict, dataclass, field
|
|
|
7
7
|
from typing import TYPE_CHECKING, Any
|
|
8
8
|
|
|
9
9
|
from ..exceptions import RuntimeErrorType, SchemaError, SchemaErrorType, format_exception
|
|
10
|
-
from ..generation import DataGenerationMethod
|
|
11
10
|
from ..internal.datetime import current_datetime
|
|
12
11
|
from ..internal.result import Err, Ok, Result
|
|
13
12
|
from .serialization import SerializedError, SerializedTestResult
|
|
14
13
|
|
|
15
14
|
if TYPE_CHECKING:
|
|
15
|
+
from ..generation import DataGenerationMethod
|
|
16
16
|
from ..models import APIOperation, Status, TestResult, TestResultSet
|
|
17
17
|
from ..schemas import BaseSchema, Specification
|
|
18
18
|
from ..service.models import AnalysisResult
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from ...models import TestResult, TestResultSet
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ...exceptions import OperationSchemaError
|
|
10
|
+
import threading
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class RunnerContext:
|
|
15
|
+
"""Holds context shared for a test run."""
|
|
16
|
+
|
|
17
|
+
data: TestResultSet
|
|
18
|
+
seed: int | None
|
|
19
|
+
stop_event: threading.Event
|
|
20
|
+
|
|
21
|
+
__slots__ = ("data", "seed", "stop_event")
|
|
22
|
+
|
|
23
|
+
def __init__(self, seed: int | None, stop_event: threading.Event) -> None:
|
|
24
|
+
self.data = TestResultSet(seed=seed)
|
|
25
|
+
self.seed = seed
|
|
26
|
+
self.stop_event = stop_event
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def is_stopped(self) -> bool:
|
|
30
|
+
return self.stop_event.is_set()
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def has_all_not_found(self) -> bool:
|
|
34
|
+
"""Check if all responses are 404."""
|
|
35
|
+
has_not_found = False
|
|
36
|
+
for entry in self.data.results:
|
|
37
|
+
for check in entry.checks:
|
|
38
|
+
if check.response is not None:
|
|
39
|
+
if check.response.status_code == 404:
|
|
40
|
+
has_not_found = True
|
|
41
|
+
else:
|
|
42
|
+
# There are non-404 responses, no reason to check any other response
|
|
43
|
+
return False
|
|
44
|
+
# Only happens if all responses are 404, or there are no responses at all.
|
|
45
|
+
# In the first case, it returns True, for the latter - False
|
|
46
|
+
return has_not_found
|
|
47
|
+
|
|
48
|
+
def add_result(self, result: TestResult) -> None:
|
|
49
|
+
self.data.append(result)
|
|
50
|
+
|
|
51
|
+
def add_generic_error(self, error: OperationSchemaError) -> None:
|
|
52
|
+
self.data.generic_errors.append(error)
|
|
53
|
+
|
|
54
|
+
def add_warning(self, message: str) -> None:
|
|
55
|
+
self.data.add_warning(message)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
ALL_NOT_FOUND_WARNING_MESSAGE = "All API responses have a 404 status code. Did you specify the proper API location?"
|