schemathesis 3.35.4__py3-none-any.whl → 3.36.0__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/checks.py +8 -5
- schemathesis/cli/__init__.py +23 -26
- 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/contrib/unique_data.py +1 -2
- 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 +53 -12
- schemathesis/graphql.py +0 -1
- schemathesis/hooks.py +3 -3
- schemathesis/internal/checks.py +53 -0
- schemathesis/lazy.py +10 -7
- schemathesis/loaders.py +3 -3
- schemathesis/models.py +59 -23
- schemathesis/runner/__init__.py +12 -6
- schemathesis/runner/events.py +1 -1
- schemathesis/runner/impl/context.py +72 -0
- schemathesis/runner/impl/core.py +105 -67
- schemathesis/runner/impl/solo.py +17 -20
- schemathesis/runner/impl/threadpool.py +65 -72
- schemathesis/runner/serialization.py +4 -3
- schemathesis/sanitization.py +2 -1
- schemathesis/schemas.py +20 -22
- 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 +5 -4
- schemathesis/specs/graphql/schemas.py +13 -8
- schemathesis/specs/openapi/checks.py +76 -27
- 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 +6 -5
- 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 +28 -13
- 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 +2 -1
- schemathesis/stateful/context.py +13 -3
- schemathesis/stateful/events.py +3 -3
- schemathesis/stateful/runner.py +24 -6
- schemathesis/stateful/sink.py +1 -1
- schemathesis/stateful/state_machine.py +7 -6
- schemathesis/stateful/statistic.py +3 -1
- schemathesis/stateful/validation.py +10 -5
- 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.36.0.dist-info}/METADATA +1 -1
- schemathesis-3.36.0.dist-info/RECORD +157 -0
- schemathesis-3.35.4.dist-info/RECORD +0 -154
- {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/WHEEL +0 -0
- {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -20,21 +20,18 @@ from typing import (
|
|
|
20
20
|
Mapping,
|
|
21
21
|
NoReturn,
|
|
22
22
|
Sequence,
|
|
23
|
-
Type,
|
|
24
23
|
TypeVar,
|
|
25
24
|
cast,
|
|
26
25
|
)
|
|
27
26
|
from urllib.parse import urlsplit
|
|
28
27
|
|
|
29
28
|
import jsonschema
|
|
30
|
-
from hypothesis.strategies import SearchStrategy
|
|
31
29
|
from packaging import version
|
|
32
30
|
from requests.structures import CaseInsensitiveDict
|
|
33
31
|
|
|
34
32
|
from ... import experimental, failures
|
|
35
33
|
from ..._compat import MultipleFailures
|
|
36
34
|
from ..._override import CaseOverride, check_no_override_mark, set_override_mark
|
|
37
|
-
from ...auths import AuthStorage
|
|
38
35
|
from ...constants import HTTP_METHODS, NOT_SET
|
|
39
36
|
from ...exceptions import (
|
|
40
37
|
InternalError,
|
|
@@ -54,10 +51,8 @@ from ...internal.result import Err, Ok, Result
|
|
|
54
51
|
from ...models import APIOperation, Case, OperationDefinition
|
|
55
52
|
from ...schemas import APIOperationMap, BaseSchema
|
|
56
53
|
from ...stateful import Stateful, StatefulTest
|
|
57
|
-
from ...stateful.state_machine import APIStateMachine
|
|
58
54
|
from ...transports.content_types import is_json_media_type, parse_content_type
|
|
59
55
|
from ...transports.responses import get_json
|
|
60
|
-
from ...types import Body, Cookies, FormData, GenericTest, Headers, NotSet, PathParameters, Query
|
|
61
56
|
from . import links, serialization
|
|
62
57
|
from ._cache import OperationCache
|
|
63
58
|
from ._hypothesis import get_case_strategy
|
|
@@ -83,7 +78,12 @@ from .security import BaseSecurityProcessor, OpenAPISecurityProcessor, SwaggerSe
|
|
|
83
78
|
from .stateful import create_state_machine
|
|
84
79
|
|
|
85
80
|
if TYPE_CHECKING:
|
|
81
|
+
from hypothesis.strategies import SearchStrategy
|
|
82
|
+
|
|
83
|
+
from ...auths import AuthStorage
|
|
84
|
+
from ...stateful.state_machine import APIStateMachine
|
|
86
85
|
from ...transports.responses import GenericResponse
|
|
86
|
+
from ...types import Body, Cookies, FormData, GenericTest, Headers, NotSet, PathParameters, Query
|
|
87
87
|
|
|
88
88
|
SCHEMA_ERROR_MESSAGE = "Ensure that the definition complies with the OpenAPI specification"
|
|
89
89
|
SCHEMA_PARSING_ERRORS = (KeyError, AttributeError, jsonschema.exceptions.RefResolutionError)
|
|
@@ -253,7 +253,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
253
253
|
return self.collect_parameters(itertools.chain(parameters, shared_parameters), operation)
|
|
254
254
|
|
|
255
255
|
def get_all_operations(
|
|
256
|
-
self, hooks: HookDispatcher | None = None
|
|
256
|
+
self, hooks: HookDispatcher | None = None, generation_config: GenerationConfig | None = None
|
|
257
257
|
) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
|
|
258
258
|
"""Iterate over all operations defined in the API.
|
|
259
259
|
|
|
@@ -308,7 +308,17 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
308
308
|
continue
|
|
309
309
|
parameters = resolved.get("parameters", ())
|
|
310
310
|
parameters = collect_parameters(itertools.chain(parameters, shared_parameters), resolved)
|
|
311
|
-
operation = make_operation(
|
|
311
|
+
operation = make_operation(
|
|
312
|
+
path,
|
|
313
|
+
method,
|
|
314
|
+
parameters,
|
|
315
|
+
entry,
|
|
316
|
+
resolved,
|
|
317
|
+
scope,
|
|
318
|
+
with_security_parameters=generation_config.with_security_parameters
|
|
319
|
+
if generation_config
|
|
320
|
+
else None,
|
|
321
|
+
)
|
|
312
322
|
context = HookContext(operation=operation)
|
|
313
323
|
if (
|
|
314
324
|
should_skip_operation(GLOBAL_HOOK_DISPATCHER, context)
|
|
@@ -383,6 +393,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
383
393
|
raw: dict[str, Any],
|
|
384
394
|
resolved: dict[str, Any],
|
|
385
395
|
scope: str,
|
|
396
|
+
with_security_parameters: bool | None = None,
|
|
386
397
|
) -> APIOperation:
|
|
387
398
|
"""Create JSON schemas for the query, body, etc from Swagger parameters definitions."""
|
|
388
399
|
__tracebackhide__ = True
|
|
@@ -397,7 +408,12 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
397
408
|
)
|
|
398
409
|
for parameter in parameters:
|
|
399
410
|
operation.add_parameter(parameter)
|
|
400
|
-
|
|
411
|
+
with_security_parameters = (
|
|
412
|
+
with_security_parameters
|
|
413
|
+
if with_security_parameters is not None
|
|
414
|
+
else self.generation_config.with_security_parameters
|
|
415
|
+
)
|
|
416
|
+
if with_security_parameters:
|
|
401
417
|
self.security.process_definitions(self.raw_schema, operation, self.resolver)
|
|
402
418
|
self.dispatch_hook("before_init_operation", HookContext(operation=operation), operation)
|
|
403
419
|
return operation
|
|
@@ -626,7 +642,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
626
642
|
return operation.definition.raw.get("tags")
|
|
627
643
|
|
|
628
644
|
@property
|
|
629
|
-
def validator_cls(self) ->
|
|
645
|
+
def validator_cls(self) -> type[jsonschema.Validator]:
|
|
630
646
|
if self.spec_version.startswith("3.1") and experimental.OPEN_API_3_1.is_enabled:
|
|
631
647
|
return jsonschema.Draft202012Validator
|
|
632
648
|
return jsonschema.Draft4Validator
|
|
@@ -791,11 +807,10 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
791
807
|
|
|
792
808
|
def _maybe_raise_one_or_more(errors: list[Exception]) -> None:
|
|
793
809
|
if not errors:
|
|
794
|
-
return
|
|
795
|
-
|
|
810
|
+
return
|
|
811
|
+
if len(errors) == 1:
|
|
796
812
|
raise errors[0]
|
|
797
|
-
|
|
798
|
-
raise MultipleFailures("\n\n".join(str(error) for error in errors), errors)
|
|
813
|
+
raise MultipleFailures("\n\n".join(str(error) for error in errors), errors)
|
|
799
814
|
|
|
800
815
|
|
|
801
816
|
def _make_reference_key(scopes: list[str], reference: str) -> str:
|
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
-
from typing import Any, ClassVar, Generator
|
|
6
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Generator
|
|
7
7
|
|
|
8
|
-
from jsonschema import RefResolver
|
|
9
|
-
|
|
10
|
-
from ...models import APIOperation
|
|
11
8
|
from .parameters import OpenAPI20Parameter, OpenAPI30Parameter, OpenAPIParameter
|
|
12
9
|
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from jsonschema import RefResolver
|
|
12
|
+
|
|
13
|
+
from ...models import APIOperation
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
@dataclass
|
|
15
17
|
class BaseSecurityProcessor:
|
|
@@ -16,11 +16,11 @@ from .. import expressions
|
|
|
16
16
|
from ..links import get_all_links
|
|
17
17
|
from ..utils import expand_status_code
|
|
18
18
|
from .statistic import OpenAPILinkStats
|
|
19
|
-
from .types import FilterFunction, LinkName, StatusCode, TargetName
|
|
20
19
|
|
|
21
20
|
if TYPE_CHECKING:
|
|
22
21
|
from ....models import Case
|
|
23
22
|
from ..schemas import BaseOpenAPISchema
|
|
23
|
+
from .types import FilterFunction, LinkName, StatusCode, TargetName
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
class OpenAPIStateMachine(APIStateMachine):
|
|
@@ -193,7 +193,7 @@ def make_response_matcher(matchers: list[tuple[str, FilterFunction]]) -> Callabl
|
|
|
193
193
|
return compare
|
|
194
194
|
|
|
195
195
|
|
|
196
|
-
@lru_cache
|
|
196
|
+
@lru_cache
|
|
197
197
|
def make_response_filter(status_code: str, all_status_codes: Iterator[str]) -> FilterFunction:
|
|
198
198
|
"""Create a filter for stored responses.
|
|
199
199
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
|
-
from typing import TYPE_CHECKING, Iterator,
|
|
4
|
+
from typing import TYPE_CHECKING, Iterator, Union
|
|
5
5
|
|
|
6
6
|
from ....internal.copy import fast_deepcopy
|
|
7
7
|
from ....stateful.statistic import TransitionStats
|
|
8
|
-
from .types import AggregatedResponseCounter, LinkName, ResponseCounter, SourceName, StatusCode, TargetName
|
|
9
8
|
|
|
10
9
|
if TYPE_CHECKING:
|
|
11
10
|
from ....stateful import events
|
|
11
|
+
from .types import AggregatedResponseCounter, LinkName, ResponseCounter, SourceName, StatusCode, TargetName
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
@dataclass
|
|
@@ -136,7 +136,7 @@ class OpenAPILinkStats(TransitionStats):
|
|
|
136
136
|
def to_formatted_table(self, width: int) -> str:
|
|
137
137
|
"""Format the statistic as a table."""
|
|
138
138
|
entries = list(self.iter_with_format())
|
|
139
|
-
lines:
|
|
139
|
+
lines: list[str | list[str]] = [HEADER, ""]
|
|
140
140
|
column_widths = [len(column) for column in HEADER]
|
|
141
141
|
for entry in entries:
|
|
142
142
|
if isinstance(entry.entry, Link):
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Callable, Dict, TypedDict, Union
|
|
3
|
+
from typing import TYPE_CHECKING, Callable, Dict, TypedDict, Union
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from ....stateful.state_machine import StepResult
|
|
6
7
|
|
|
7
8
|
StatusCode = str
|
|
8
9
|
LinkName = str
|
|
@@ -5,15 +5,15 @@ import json
|
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
from typing import TYPE_CHECKING, Any, Callable, Generator
|
|
7
7
|
|
|
8
|
-
from .. import GenerationConfig
|
|
9
8
|
from ..constants import NOT_SET
|
|
10
|
-
from ..exceptions import OperationSchemaError
|
|
11
9
|
from ..internal.result import Ok, Result
|
|
12
|
-
from ..models import APIOperation, Case
|
|
13
10
|
|
|
14
11
|
if TYPE_CHECKING:
|
|
15
12
|
import hypothesis
|
|
16
13
|
|
|
14
|
+
from .. import GenerationConfig
|
|
15
|
+
from ..exceptions import OperationSchemaError
|
|
16
|
+
from ..models import APIOperation, Case
|
|
17
17
|
from ..transports.responses import GenericResponse
|
|
18
18
|
from .state_machine import APIStateMachine
|
|
19
19
|
|
schemathesis/stateful/config.py
CHANGED
|
@@ -21,7 +21,7 @@ def _default_checks_factory() -> tuple[CheckFunction, ...]:
|
|
|
21
21
|
from ..checks import ALL_CHECKS
|
|
22
22
|
from ..specs.openapi.checks import ensure_resource_availability, use_after_free
|
|
23
23
|
|
|
24
|
-
return ALL_CHECKS
|
|
24
|
+
return (*ALL_CHECKS, use_after_free, ensure_resource_availability)
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def _get_default_hypothesis_settings_kwargs() -> dict[str, Any]:
|
|
@@ -69,6 +69,7 @@ class StatefulTestRunnerConfig:
|
|
|
69
69
|
max_response_time: int | None = None
|
|
70
70
|
dry_run: bool = False
|
|
71
71
|
targets: list[Target] = field(default_factory=list)
|
|
72
|
+
unique_data: bool = False
|
|
72
73
|
|
|
73
74
|
def __post_init__(self) -> None:
|
|
74
75
|
import hypothesis
|
schemathesis/stateful/context.py
CHANGED
|
@@ -4,6 +4,7 @@ import traceback
|
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
from typing import TYPE_CHECKING, Tuple, Type, Union
|
|
6
6
|
|
|
7
|
+
from ..constants import NOT_SET
|
|
7
8
|
from ..exceptions import CheckFailed
|
|
8
9
|
from ..targets import TargetMetricCollector
|
|
9
10
|
from . import events
|
|
@@ -11,6 +12,7 @@ from . import events
|
|
|
11
12
|
if TYPE_CHECKING:
|
|
12
13
|
from ..models import Case, Check
|
|
13
14
|
from ..transports.responses import GenericResponse
|
|
15
|
+
from ..types import NotSet
|
|
14
16
|
|
|
15
17
|
FailureKey = Union[Type[CheckFailed], Tuple[str, int]]
|
|
16
18
|
|
|
@@ -52,16 +54,17 @@ class RunnerContext:
|
|
|
52
54
|
completed_scenarios: int = 0
|
|
53
55
|
# Metrics collector for targeted testing
|
|
54
56
|
metric_collector: TargetMetricCollector = field(default_factory=lambda: TargetMetricCollector(targets=[]))
|
|
57
|
+
step_outcomes: dict[int, BaseException | None] = field(default_factory=dict)
|
|
55
58
|
|
|
56
59
|
@property
|
|
57
60
|
def current_scenario_status(self) -> events.ScenarioStatus:
|
|
58
61
|
if self.current_step_status == events.StepStatus.SUCCESS:
|
|
59
62
|
return events.ScenarioStatus.SUCCESS
|
|
60
|
-
|
|
63
|
+
if self.current_step_status == events.StepStatus.FAILURE:
|
|
61
64
|
return events.ScenarioStatus.FAILURE
|
|
62
|
-
|
|
65
|
+
if self.current_step_status == events.StepStatus.ERROR:
|
|
63
66
|
return events.ScenarioStatus.ERROR
|
|
64
|
-
|
|
67
|
+
if self.current_step_status == events.StepStatus.INTERRUPTED:
|
|
65
68
|
return events.ScenarioStatus.INTERRUPTED
|
|
66
69
|
return events.ScenarioStatus.REJECTED
|
|
67
70
|
|
|
@@ -69,6 +72,7 @@ class RunnerContext:
|
|
|
69
72
|
self.completed_scenarios += 1
|
|
70
73
|
self.current_step_status = None
|
|
71
74
|
self.current_response = None
|
|
75
|
+
self.step_outcomes.clear()
|
|
72
76
|
|
|
73
77
|
def reset_step(self) -> None:
|
|
74
78
|
self.checks_for_step = []
|
|
@@ -123,3 +127,9 @@ class RunnerContext:
|
|
|
123
127
|
self.seen_in_suite.clear()
|
|
124
128
|
self.reset_scenario()
|
|
125
129
|
self.metric_collector.reset()
|
|
130
|
+
|
|
131
|
+
def store_step_outcome(self, case: Case, outcome: BaseException | None) -> None:
|
|
132
|
+
self.step_outcomes[hash(case)] = outcome
|
|
133
|
+
|
|
134
|
+
def get_step_outcome(self, case: Case) -> BaseException | None | NotSet:
|
|
135
|
+
return self.step_outcomes.get(hash(case), NOT_SET)
|
schemathesis/stateful/events.py
CHANGED
|
@@ -4,7 +4,7 @@ import time
|
|
|
4
4
|
from dataclasses import asdict as _asdict
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from enum import Enum
|
|
7
|
-
from typing import TYPE_CHECKING, Any
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
8
|
|
|
9
9
|
from ..exceptions import format_exception
|
|
10
10
|
|
|
@@ -40,11 +40,11 @@ class RunStarted(StatefulEvent):
|
|
|
40
40
|
"""Before executing all scenarios."""
|
|
41
41
|
|
|
42
42
|
started_at: float
|
|
43
|
-
state_machine:
|
|
43
|
+
state_machine: type[APIStateMachine]
|
|
44
44
|
|
|
45
45
|
__slots__ = ("state_machine", "timestamp", "started_at")
|
|
46
46
|
|
|
47
|
-
def __init__(self, *, state_machine:
|
|
47
|
+
def __init__(self, *, state_machine: type[APIStateMachine]) -> None:
|
|
48
48
|
self.state_machine = state_machine
|
|
49
49
|
self.started_at = time.time()
|
|
50
50
|
self.timestamp = time.monotonic()
|
schemathesis/stateful/runner.py
CHANGED
|
@@ -4,13 +4,12 @@ import queue
|
|
|
4
4
|
import threading
|
|
5
5
|
from contextlib import contextmanager
|
|
6
6
|
from dataclasses import dataclass, field
|
|
7
|
-
from typing import TYPE_CHECKING, Any, Generator, Iterator
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Generator, Iterator
|
|
8
8
|
|
|
9
9
|
import hypothesis
|
|
10
10
|
import requests
|
|
11
11
|
from hypothesis.control import current_build_context
|
|
12
12
|
from hypothesis.errors import Flaky, Unsatisfiable
|
|
13
|
-
from hypothesis.stateful import Rule
|
|
14
13
|
|
|
15
14
|
from ..exceptions import CheckFailed
|
|
16
15
|
from ..targets import TargetMetricCollector
|
|
@@ -20,6 +19,8 @@ from .context import RunnerContext
|
|
|
20
19
|
from .validation import validate_response
|
|
21
20
|
|
|
22
21
|
if TYPE_CHECKING:
|
|
22
|
+
from hypothesis.stateful import Rule
|
|
23
|
+
|
|
23
24
|
from ..models import Case, CheckFunction
|
|
24
25
|
from ..transports.responses import GenericResponse
|
|
25
26
|
from .state_machine import APIStateMachine, Direction, StepResult
|
|
@@ -36,7 +37,7 @@ class StatefulTestRunner:
|
|
|
36
37
|
"""
|
|
37
38
|
|
|
38
39
|
# State machine class to use
|
|
39
|
-
state_machine:
|
|
40
|
+
state_machine: type[APIStateMachine]
|
|
40
41
|
# Test runner configuration that defines the runtime behavior
|
|
41
42
|
config: StatefulTestRunnerConfig = field(default_factory=StatefulTestRunnerConfig)
|
|
42
43
|
# Event to stop the execution
|
|
@@ -105,7 +106,7 @@ def thread_manager(thread: threading.Thread) -> Generator[None, None, None]:
|
|
|
105
106
|
|
|
106
107
|
def _execute_state_machine_loop(
|
|
107
108
|
*,
|
|
108
|
-
state_machine:
|
|
109
|
+
state_machine: type[APIStateMachine],
|
|
109
110
|
event_queue: queue.Queue,
|
|
110
111
|
config: StatefulTestRunnerConfig,
|
|
111
112
|
stop_event: threading.Event,
|
|
@@ -162,17 +163,34 @@ def _execute_state_machine_loop(
|
|
|
162
163
|
try:
|
|
163
164
|
if config.dry_run:
|
|
164
165
|
return None
|
|
166
|
+
if config.unique_data:
|
|
167
|
+
cached = ctx.get_step_outcome(case)
|
|
168
|
+
if isinstance(cached, BaseException):
|
|
169
|
+
raise cached
|
|
170
|
+
elif cached is None:
|
|
171
|
+
return None
|
|
165
172
|
result = super().step(case, previous)
|
|
166
173
|
ctx.step_succeeded()
|
|
167
|
-
except CheckFailed:
|
|
174
|
+
except CheckFailed as exc:
|
|
175
|
+
if config.unique_data:
|
|
176
|
+
ctx.store_step_outcome(case, exc)
|
|
168
177
|
ctx.step_failed()
|
|
169
178
|
raise
|
|
170
|
-
except Exception:
|
|
179
|
+
except Exception as exc:
|
|
180
|
+
if config.unique_data:
|
|
181
|
+
ctx.store_step_outcome(case, exc)
|
|
171
182
|
ctx.step_errored()
|
|
172
183
|
raise
|
|
173
184
|
except KeyboardInterrupt:
|
|
174
185
|
ctx.step_interrupted()
|
|
175
186
|
raise
|
|
187
|
+
except BaseException as exc:
|
|
188
|
+
if config.unique_data:
|
|
189
|
+
ctx.store_step_outcome(case, exc)
|
|
190
|
+
raise exc
|
|
191
|
+
else:
|
|
192
|
+
if config.unique_data:
|
|
193
|
+
ctx.store_step_outcome(case, None)
|
|
176
194
|
finally:
|
|
177
195
|
transition_id: events.TransitionId | None
|
|
178
196
|
if previous is not None:
|
schemathesis/stateful/sink.py
CHANGED
|
@@ -4,7 +4,7 @@ import re
|
|
|
4
4
|
import time
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from functools import lru_cache
|
|
7
|
-
from typing import TYPE_CHECKING, Any, ClassVar
|
|
7
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
8
8
|
|
|
9
9
|
from hypothesis.errors import InvalidDefinition
|
|
10
10
|
from hypothesis.stateful import RuleBasedStateMachine
|
|
@@ -12,11 +12,11 @@ from hypothesis.stateful import RuleBasedStateMachine
|
|
|
12
12
|
from .._dependency_versions import HYPOTHESIS_HAS_STATEFUL_NAMING_IMPROVEMENTS
|
|
13
13
|
from ..constants import NO_LINKS_ERROR_MESSAGE, NOT_SET
|
|
14
14
|
from ..exceptions import UsageError
|
|
15
|
-
from ..
|
|
15
|
+
from ..internal.checks import CheckFunction
|
|
16
|
+
from ..models import APIOperation, Case
|
|
16
17
|
from .config import _default_hypothesis_settings_factory
|
|
17
18
|
from .runner import StatefulTestRunner, StatefulTestRunnerConfig
|
|
18
19
|
from .sink import StateMachineSink
|
|
19
|
-
from .statistic import TransitionStats
|
|
20
20
|
|
|
21
21
|
if TYPE_CHECKING:
|
|
22
22
|
import hypothesis
|
|
@@ -24,6 +24,7 @@ if TYPE_CHECKING:
|
|
|
24
24
|
|
|
25
25
|
from ..schemas import BaseSchema
|
|
26
26
|
from ..transports.responses import GenericResponse
|
|
27
|
+
from .statistic import TransitionStats
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
@dataclass
|
|
@@ -64,7 +65,7 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
|
64
65
|
|
|
65
66
|
@classmethod
|
|
66
67
|
@lru_cache
|
|
67
|
-
def _to_test_case(cls) ->
|
|
68
|
+
def _to_test_case(cls) -> type:
|
|
68
69
|
from . import run_state_machine_as_test
|
|
69
70
|
|
|
70
71
|
class StateMachineTestCase(RuleBasedStateMachine.TestCase):
|
|
@@ -97,7 +98,7 @@ class APIStateMachine(RuleBasedStateMachine):
|
|
|
97
98
|
|
|
98
99
|
def _add_result_to_targets(self, targets: tuple[str, ...], result: StepResult | None) -> None:
|
|
99
100
|
if result is None:
|
|
100
|
-
return
|
|
101
|
+
return
|
|
101
102
|
target = self._get_target_for_result(result)
|
|
102
103
|
if target is not None:
|
|
103
104
|
super()._add_result_to_targets((target,), result)
|
|
@@ -310,7 +311,7 @@ def _print_case(case: Case, kwargs: dict[str, Any]) -> str:
|
|
|
310
311
|
headers.update(kwargs.get("headers", {}))
|
|
311
312
|
case.headers = headers
|
|
312
313
|
data = [
|
|
313
|
-
f"{name}={
|
|
314
|
+
f"{name}={getattr(case, name)!r}"
|
|
314
315
|
for name in ("path_parameters", "headers", "cookies", "query", "body", "media_type")
|
|
315
316
|
if getattr(case, name) not in (None, NOT_SET)
|
|
316
317
|
]
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import TYPE_CHECKING
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
4
|
|
|
5
5
|
from ..exceptions import CheckFailed, get_grouped_exception
|
|
6
|
-
from .
|
|
6
|
+
from ..internal.checks import CheckContext
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
9
|
from ..failures import FailureContext
|
|
10
|
-
from ..
|
|
10
|
+
from ..internal.checks import CheckFunction
|
|
11
|
+
from ..models import Case
|
|
11
12
|
from ..transports.responses import GenericResponse
|
|
13
|
+
from .context import RunnerContext
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
def validate_response(
|
|
@@ -19,8 +21,11 @@ def validate_response(
|
|
|
19
21
|
checks: tuple[CheckFunction, ...],
|
|
20
22
|
additional_checks: tuple[CheckFunction, ...] = (),
|
|
21
23
|
max_response_time: int | None = None,
|
|
24
|
+
headers: dict[str, Any] | None = None,
|
|
22
25
|
) -> None:
|
|
23
26
|
"""Validate the response against the provided checks."""
|
|
27
|
+
from requests.structures import CaseInsensitiveDict
|
|
28
|
+
|
|
24
29
|
from .._compat import MultipleFailures
|
|
25
30
|
from ..checks import _make_max_response_time_failure_message
|
|
26
31
|
from ..failures import ResponseTimeExceeded
|
|
@@ -28,6 +33,7 @@ def validate_response(
|
|
|
28
33
|
|
|
29
34
|
exceptions: list[CheckFailed | AssertionError] = []
|
|
30
35
|
check_results = ctx.checks_for_step
|
|
36
|
+
check_ctx = CheckContext(headers=CaseInsensitiveDict(headers) if headers else None)
|
|
31
37
|
|
|
32
38
|
def _on_failure(exc: CheckFailed | AssertionError, message: str, context: FailureContext | None) -> None:
|
|
33
39
|
exceptions.append(exc)
|
|
@@ -62,8 +68,7 @@ def validate_response(
|
|
|
62
68
|
name = check.__name__
|
|
63
69
|
copied_case = case.partial_deepcopy()
|
|
64
70
|
try:
|
|
65
|
-
check(response, copied_case)
|
|
66
|
-
skip_check = check(response, copied_case)
|
|
71
|
+
skip_check = check(check_ctx, response, copied_case)
|
|
67
72
|
if not skip_check:
|
|
68
73
|
_on_passed(name, copied_case)
|
|
69
74
|
except CheckFailed as exc:
|
|
@@ -101,7 +101,7 @@ class RequestsTransport:
|
|
|
101
101
|
cookies: dict[str, Any] | None = None,
|
|
102
102
|
) -> dict[str, Any]:
|
|
103
103
|
final_headers = case._get_headers(headers)
|
|
104
|
-
media_type:
|
|
104
|
+
media_type: str | None
|
|
105
105
|
if case.body is not NOT_SET and case.media_type is None:
|
|
106
106
|
media_type = case.operation._get_default_media_type()
|
|
107
107
|
else:
|
|
@@ -274,7 +274,7 @@ class WSGITransport:
|
|
|
274
274
|
cookies: dict[str, Any] | None = None,
|
|
275
275
|
) -> dict[str, Any]:
|
|
276
276
|
final_headers = case._get_headers(headers)
|
|
277
|
-
media_type:
|
|
277
|
+
media_type: str | None
|
|
278
278
|
if case.body is not NOT_SET and case.media_type is None:
|
|
279
279
|
media_type = case.operation._get_default_media_type()
|
|
280
280
|
else:
|
schemathesis/transports/auth.py
CHANGED
|
@@ -3,11 +3,12 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING, Any
|
|
4
4
|
|
|
5
5
|
from ..constants import USER_AGENT
|
|
6
|
-
from ..types import RawAuth
|
|
7
6
|
|
|
8
7
|
if TYPE_CHECKING:
|
|
9
8
|
from requests.auth import HTTPDigestAuth
|
|
10
9
|
|
|
10
|
+
from ..types import RawAuth
|
|
11
|
+
|
|
11
12
|
|
|
12
13
|
def get_requests_auth(auth: RawAuth | None, auth_type: str | None) -> HTTPDigestAuth | RawAuth | None:
|
|
13
14
|
from requests.auth import HTTPDigestAuth
|
|
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import sys
|
|
5
|
-
from datetime import timedelta
|
|
6
5
|
from typing import TYPE_CHECKING, Any, NoReturn, Union
|
|
7
6
|
|
|
8
7
|
from werkzeug.wrappers import Response as BaseResponse
|
|
@@ -10,6 +9,8 @@ from werkzeug.wrappers import Response as BaseResponse
|
|
|
10
9
|
from .._compat import JSONMixin
|
|
11
10
|
|
|
12
11
|
if TYPE_CHECKING:
|
|
12
|
+
from datetime import timedelta
|
|
13
|
+
|
|
13
14
|
from httpx import Response as httpxResponse
|
|
14
15
|
from requests import PreparedRequest
|
|
15
16
|
from requests import Response as requestsResponse
|
schemathesis/utils.py
CHANGED
|
@@ -5,6 +5,7 @@ from contextlib import contextmanager
|
|
|
5
5
|
from inspect import getfullargspec
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import (
|
|
8
|
+
TYPE_CHECKING,
|
|
8
9
|
Any,
|
|
9
10
|
Callable,
|
|
10
11
|
Generator,
|
|
@@ -20,9 +21,10 @@ from hypothesis.strategies import SearchStrategy
|
|
|
20
21
|
from ._compat import InferType, get_signature
|
|
21
22
|
|
|
22
23
|
# Backward-compat
|
|
23
|
-
from .constants import NOT_SET # noqa: F401
|
|
24
24
|
from .exceptions import SkipTest, UsageError
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from .types import GenericTest, PathLike
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
def is_schemathesis_test(func: Callable) -> bool:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: schemathesis
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.36.0
|
|
4
4
|
Summary: Property-based testing framework for Open API and GraphQL based apps
|
|
5
5
|
Project-URL: Documentation, https://schemathesis.readthedocs.io/en/stable/
|
|
6
6
|
Project-URL: Changelog, https://schemathesis.readthedocs.io/en/stable/changelog.html
|