schemathesis 3.25.6__py3-none-any.whl → 4.0.0a1__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 +27 -65
- schemathesis/auths.py +102 -82
- schemathesis/checks.py +126 -46
- schemathesis/cli/__init__.py +11 -1760
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +37 -0
- schemathesis/cli/commands/run/__init__.py +662 -0
- schemathesis/cli/commands/run/checks.py +80 -0
- schemathesis/cli/commands/run/context.py +117 -0
- schemathesis/cli/commands/run/events.py +35 -0
- schemathesis/cli/commands/run/executor.py +138 -0
- schemathesis/cli/commands/run/filters.py +194 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
- schemathesis/cli/commands/run/handlers/output.py +746 -0
- schemathesis/cli/commands/run/hypothesis.py +105 -0
- schemathesis/cli/commands/run/loaders.py +129 -0
- schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
- schemathesis/cli/constants.py +5 -52
- schemathesis/cli/core.py +17 -0
- schemathesis/cli/ext/fs.py +14 -0
- schemathesis/cli/ext/groups.py +55 -0
- schemathesis/cli/{options.py → ext/options.py} +39 -10
- schemathesis/cli/hooks.py +36 -0
- schemathesis/contrib/__init__.py +1 -3
- schemathesis/contrib/openapi/__init__.py +1 -3
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
- schemathesis/core/__init__.py +58 -0
- schemathesis/core/compat.py +25 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +370 -0
- schemathesis/core/failures.py +285 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
- schemathesis/core/output/__init__.py +69 -0
- schemathesis/core/output/sanitization.py +197 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +31 -0
- schemathesis/{internal → core}/result.py +1 -1
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +108 -0
- schemathesis/core/validation.py +38 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +30 -0
- schemathesis/engine/config.py +59 -0
- schemathesis/engine/context.py +119 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +157 -0
- schemathesis/engine/errors.py +394 -0
- schemathesis/engine/events.py +337 -0
- schemathesis/engine/phases/__init__.py +66 -0
- schemathesis/{runner → engine/phases}/probes.py +50 -67
- schemathesis/engine/phases/stateful/__init__.py +65 -0
- schemathesis/engine/phases/stateful/_executor.py +326 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +174 -0
- schemathesis/engine/phases/unit/_executor.py +321 -0
- schemathesis/engine/phases/unit/_pool.py +74 -0
- schemathesis/engine/recorder.py +241 -0
- schemathesis/errors.py +31 -0
- schemathesis/experimental/__init__.py +18 -14
- schemathesis/filters.py +103 -14
- schemathesis/generation/__init__.py +21 -37
- schemathesis/generation/case.py +190 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/generation/hypothesis/__init__.py +30 -0
- schemathesis/generation/hypothesis/builder.py +585 -0
- schemathesis/generation/hypothesis/examples.py +50 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/modes.py +28 -0
- schemathesis/generation/overrides.py +96 -0
- schemathesis/generation/stateful/__init__.py +20 -0
- schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
- schemathesis/generation/targets.py +69 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +115 -0
- schemathesis/graphql/loaders.py +131 -0
- schemathesis/hooks.py +99 -67
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +412 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +63 -0
- schemathesis/openapi/loaders.py +178 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +273 -0
- schemathesis/pytest/loaders.py +12 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -261
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +25 -0
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +7 -5
- schemathesis/specs/graphql/schemas.py +215 -187
- schemathesis/specs/graphql/validation.py +11 -18
- schemathesis/specs/openapi/__init__.py +7 -1
- schemathesis/specs/openapi/_cache.py +122 -0
- schemathesis/specs/openapi/_hypothesis.py +146 -165
- schemathesis/specs/openapi/checks.py +565 -67
- schemathesis/specs/openapi/converter.py +33 -6
- schemathesis/specs/openapi/definitions.py +11 -18
- schemathesis/specs/openapi/examples.py +139 -23
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +4 -6
- schemathesis/specs/openapi/expressions/extractors.py +23 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +38 -14
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +45 -0
- schemathesis/specs/openapi/links.py +65 -165
- schemathesis/specs/openapi/media_types.py +32 -0
- schemathesis/specs/openapi/negative/__init__.py +7 -3
- schemathesis/specs/openapi/negative/mutations.py +24 -8
- schemathesis/specs/openapi/parameters.py +46 -30
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +47 -57
- schemathesis/specs/openapi/schemas.py +478 -369
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +11 -6
- schemathesis/specs/openapi/stateful/__init__.py +185 -73
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/transport/__init__.py +104 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +99 -0
- schemathesis/transport/requests.py +221 -0
- schemathesis/{_xml.py → transport/serialization.py} +143 -28
- schemathesis/transport/wsgi.py +165 -0
- schemathesis-4.0.0a1.dist-info/METADATA +297 -0
- schemathesis-4.0.0a1.dist-info/RECORD +152 -0
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -17
- schemathesis/_hypothesis.py +0 -246
- schemathesis/_override.py +0 -49
- schemathesis/cli/cassettes.py +0 -375
- schemathesis/cli/context.py +0 -58
- schemathesis/cli/debug.py +0 -26
- schemathesis/cli/handlers.py +0 -16
- schemathesis/cli/junitxml.py +0 -43
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -790
- schemathesis/cli/output/short.py +0 -44
- schemathesis/cli/sanitization.py +0 -20
- schemathesis/code_samples.py +0 -149
- schemathesis/constants.py +0 -55
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -15
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -560
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -17
- schemathesis/failures.py +0 -209
- schemathesis/fixups/__init__.py +0 -36
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -29
- schemathesis/graphql.py +0 -4
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/copy.py +0 -13
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -34
- schemathesis/internal/jsonschema.py +0 -35
- schemathesis/internal/transformation.py +0 -15
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -361
- schemathesis/loaders.py +0 -120
- schemathesis/models.py +0 -1234
- schemathesis/parameters.py +0 -86
- schemathesis/runner/__init__.py +0 -570
- schemathesis/runner/events.py +0 -329
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -1035
- schemathesis/runner/impl/solo.py +0 -90
- schemathesis/runner/impl/threadpool.py +0 -400
- schemathesis/runner/serialization.py +0 -411
- schemathesis/sanitization.py +0 -248
- schemathesis/serializers.py +0 -323
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -201
- schemathesis/service/client.py +0 -100
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -57
- schemathesis/service/hosts.py +0 -107
- schemathesis/service/metadata.py +0 -46
- schemathesis/service/models.py +0 -49
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -199
- schemathesis/service/usage.py +0 -65
- schemathesis/specs/graphql/loaders.py +0 -344
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/loaders.py +0 -667
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis/specs/openapi/validation.py +0 -25
- schemathesis/stateful/__init__.py +0 -133
- schemathesis/targets.py +0 -45
- schemathesis/throttling.py +0 -41
- schemathesis/transports/__init__.py +0 -5
- schemathesis/transports/auth.py +0 -15
- schemathesis/transports/headers.py +0 -35
- schemathesis/transports/responses.py +0 -52
- schemathesis/types.py +0 -35
- schemathesis/utils.py +0 -169
- schemathesis-3.25.6.dist-info/METADATA +0 -356
- schemathesis-3.25.6.dist-info/RECORD +0 -134
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -1,92 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
from dataclasses import dataclass
|
3
|
-
from typing import TYPE_CHECKING, Callable, Dict, List
|
4
|
-
|
5
|
-
import hypothesis.strategies as st
|
6
|
-
from requests.structures import CaseInsensitiveDict
|
7
|
-
|
8
|
-
from ..links import OpenAPILink, get_all_links
|
9
|
-
from ..utils import expand_status_code
|
10
|
-
|
11
|
-
if TYPE_CHECKING:
|
12
|
-
from ....stateful.state_machine import StepResult
|
13
|
-
from ....models import APIOperation
|
14
|
-
|
15
|
-
FilterFunction = Callable[["StepResult"], bool]
|
16
|
-
|
17
|
-
|
18
|
-
@dataclass
|
19
|
-
class Connection:
|
20
|
-
source: str
|
21
|
-
strategy: st.SearchStrategy[tuple[StepResult, OpenAPILink]]
|
22
|
-
|
23
|
-
|
24
|
-
APIOperationConnections = Dict[str, List[Connection]]
|
25
|
-
|
26
|
-
|
27
|
-
def apply(
|
28
|
-
operation: APIOperation,
|
29
|
-
bundles: dict[str, CaseInsensitiveDict],
|
30
|
-
connections: APIOperationConnections,
|
31
|
-
) -> None:
|
32
|
-
"""Gather all connections based on Open API links definitions."""
|
33
|
-
all_status_codes = list(operation.definition.resolved["responses"])
|
34
|
-
for status_code, link in get_all_links(operation):
|
35
|
-
target_operation = link.get_target_operation()
|
36
|
-
strategy = bundles[operation.path][operation.method.upper()].filter(
|
37
|
-
make_response_filter(status_code, all_status_codes)
|
38
|
-
)
|
39
|
-
connection = Connection(source=operation.verbose_name, strategy=_convert_strategy(strategy, link))
|
40
|
-
connections[target_operation.verbose_name].append(connection)
|
41
|
-
|
42
|
-
|
43
|
-
def _convert_strategy(
|
44
|
-
strategy: st.SearchStrategy[StepResult], link: OpenAPILink
|
45
|
-
) -> st.SearchStrategy[tuple[StepResult, OpenAPILink]]:
|
46
|
-
# This function is required to capture values properly (it won't work properly when lambda is defined in a loop)
|
47
|
-
return strategy.map(lambda out: (out, link))
|
48
|
-
|
49
|
-
|
50
|
-
def make_response_filter(status_code: str, all_status_codes: list[str]) -> FilterFunction:
|
51
|
-
"""Create a filter for stored responses.
|
52
|
-
|
53
|
-
This filter will decide whether some response is suitable to use as a source for requesting some API operation.
|
54
|
-
"""
|
55
|
-
if status_code == "default":
|
56
|
-
return default_status_code(all_status_codes)
|
57
|
-
return match_status_code(status_code)
|
58
|
-
|
59
|
-
|
60
|
-
def match_status_code(status_code: str) -> FilterFunction:
|
61
|
-
"""Create a filter function that matches all responses with the given status code.
|
62
|
-
|
63
|
-
Note that the status code can contain "X", which means any digit.
|
64
|
-
For example, 50X will match all status codes from 500 to 509.
|
65
|
-
"""
|
66
|
-
status_codes = set(expand_status_code(status_code))
|
67
|
-
|
68
|
-
def compare(result: StepResult) -> bool:
|
69
|
-
return result.response.status_code in status_codes
|
70
|
-
|
71
|
-
# This name is displayed in the resulting strategy representation. For example, if you run your tests with
|
72
|
-
# `--hypothesis-show-statistics`, then you can see `Bundle(name='GET /users/{user_id}').filter(match_200_response)`
|
73
|
-
# which gives you information about the particularly used filter.
|
74
|
-
compare.__name__ = f"match_{status_code}_response"
|
75
|
-
|
76
|
-
return compare
|
77
|
-
|
78
|
-
|
79
|
-
def default_status_code(status_codes: list[str]) -> FilterFunction:
|
80
|
-
"""Create a filter that matches all "default" responses.
|
81
|
-
|
82
|
-
In Open API, the "default" response is the one that is used if no other options were matched.
|
83
|
-
Therefore we need to match only responses that were not matched by other listed status codes.
|
84
|
-
"""
|
85
|
-
expanded_status_codes = {
|
86
|
-
status_code for value in status_codes if value != "default" for status_code in expand_status_code(value)
|
87
|
-
}
|
88
|
-
|
89
|
-
def match_default_response(result: StepResult) -> bool:
|
90
|
-
return result.response.status_code not in expanded_status_codes
|
91
|
-
|
92
|
-
return match_default_response
|
@@ -1,25 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
from typing import Any
|
3
|
-
|
4
|
-
from ...constants import HTTP_METHODS
|
5
|
-
|
6
|
-
|
7
|
-
def is_pattern_error(exception: TypeError) -> bool:
|
8
|
-
"""Detect whether the input exception was caused by invalid type passed to `re.search`."""
|
9
|
-
# This is intentionally simplistic and do not involve any traceback analysis
|
10
|
-
return "expected string or bytes-like object" in str(exception)
|
11
|
-
|
12
|
-
|
13
|
-
def find_numeric_http_status_codes(schema: Any) -> list[tuple[int, list[str | int]]]:
|
14
|
-
if not isinstance(schema, dict):
|
15
|
-
return []
|
16
|
-
found = []
|
17
|
-
for path, methods in schema.get("paths", {}).items():
|
18
|
-
if isinstance(methods, dict):
|
19
|
-
for method, definition in methods.items():
|
20
|
-
if method not in HTTP_METHODS or not isinstance(definition, dict):
|
21
|
-
continue
|
22
|
-
for key in definition.get("responses", {}):
|
23
|
-
if isinstance(key, int):
|
24
|
-
found.append((key, [path, method]))
|
25
|
-
return found
|
@@ -1,133 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
import enum
|
3
|
-
import json
|
4
|
-
from dataclasses import dataclass, field
|
5
|
-
from typing import TYPE_CHECKING, Any, Callable, Generator
|
6
|
-
|
7
|
-
from .. import GenerationConfig
|
8
|
-
from ..exceptions import OperationSchemaError
|
9
|
-
from ..models import APIOperation, Case
|
10
|
-
from ..constants import NOT_SET
|
11
|
-
from ..internal.result import Ok, Result
|
12
|
-
|
13
|
-
if TYPE_CHECKING:
|
14
|
-
import hypothesis
|
15
|
-
from ..transports.responses import GenericResponse
|
16
|
-
from .state_machine import APIStateMachine
|
17
|
-
|
18
|
-
|
19
|
-
@enum.unique
|
20
|
-
class Stateful(enum.Enum):
|
21
|
-
none = 1
|
22
|
-
links = 2
|
23
|
-
|
24
|
-
|
25
|
-
@dataclass
|
26
|
-
class ParsedData:
|
27
|
-
"""A structure that holds information parsed from a test outcome.
|
28
|
-
|
29
|
-
It is used later to create a new version of an API operation that will reuse this data.
|
30
|
-
"""
|
31
|
-
|
32
|
-
parameters: dict[str, Any]
|
33
|
-
body: Any = NOT_SET
|
34
|
-
|
35
|
-
def __hash__(self) -> int:
|
36
|
-
"""Custom hash simplifies deduplication of parsed data."""
|
37
|
-
value = hash(tuple(self.parameters.items())) # parameters never contain nested dicts / lists
|
38
|
-
if self.body is not NOT_SET:
|
39
|
-
if isinstance(self.body, (dict, list)):
|
40
|
-
# The simplest way to get a hash of a potentially nested structure
|
41
|
-
value ^= hash(json.dumps(self.body, sort_keys=True))
|
42
|
-
else:
|
43
|
-
# These types should be hashable
|
44
|
-
value ^= hash(self.body)
|
45
|
-
return value
|
46
|
-
|
47
|
-
|
48
|
-
@dataclass
|
49
|
-
class StatefulTest:
|
50
|
-
"""A template for a test that will be executed after another one by reusing the outcomes from it."""
|
51
|
-
|
52
|
-
name: str
|
53
|
-
|
54
|
-
def parse(self, case: Case, response: GenericResponse) -> ParsedData:
|
55
|
-
raise NotImplementedError
|
56
|
-
|
57
|
-
def make_operation(self, collected: list[ParsedData]) -> APIOperation:
|
58
|
-
raise NotImplementedError
|
59
|
-
|
60
|
-
|
61
|
-
@dataclass
|
62
|
-
class StatefulData:
|
63
|
-
"""Storage for data that will be used in later tests."""
|
64
|
-
|
65
|
-
stateful_test: StatefulTest
|
66
|
-
container: list[ParsedData] = field(default_factory=list)
|
67
|
-
|
68
|
-
def make_operation(self) -> APIOperation:
|
69
|
-
return self.stateful_test.make_operation(self.container)
|
70
|
-
|
71
|
-
def store(self, case: Case, response: GenericResponse) -> None:
|
72
|
-
"""Parse and store data for a stateful test."""
|
73
|
-
parsed = self.stateful_test.parse(case, response)
|
74
|
-
self.container.append(parsed)
|
75
|
-
|
76
|
-
|
77
|
-
@dataclass
|
78
|
-
class Feedback:
|
79
|
-
"""Handler for feedback from tests.
|
80
|
-
|
81
|
-
Provides a way to control runner's behavior from tests.
|
82
|
-
"""
|
83
|
-
|
84
|
-
stateful: Stateful | None
|
85
|
-
operation: APIOperation = field(repr=False)
|
86
|
-
stateful_tests: dict[str, StatefulData] = field(default_factory=dict, repr=False)
|
87
|
-
|
88
|
-
def add_test_case(self, case: Case, response: GenericResponse) -> None:
|
89
|
-
"""Store test data to reuse it in the future additional tests."""
|
90
|
-
for stateful_test in case.operation.get_stateful_tests(response, self.stateful):
|
91
|
-
data = self.stateful_tests.setdefault(stateful_test.name, StatefulData(stateful_test))
|
92
|
-
data.store(case, response)
|
93
|
-
|
94
|
-
def get_stateful_tests(
|
95
|
-
self,
|
96
|
-
test: Callable,
|
97
|
-
settings: hypothesis.settings | None,
|
98
|
-
generation_config: GenerationConfig | None,
|
99
|
-
seed: int | None,
|
100
|
-
as_strategy_kwargs: dict[str, Any] | Callable[[APIOperation], dict[str, Any]] | None,
|
101
|
-
) -> Generator[Result[tuple[APIOperation, Callable], OperationSchemaError], None, None]:
|
102
|
-
"""Generate additional tests that use data from the previous ones."""
|
103
|
-
from .._hypothesis import create_test
|
104
|
-
|
105
|
-
for data in self.stateful_tests.values():
|
106
|
-
operation = data.make_operation()
|
107
|
-
_as_strategy_kwargs: dict[str, Any] | None
|
108
|
-
if callable(as_strategy_kwargs):
|
109
|
-
_as_strategy_kwargs = as_strategy_kwargs(operation)
|
110
|
-
else:
|
111
|
-
_as_strategy_kwargs = as_strategy_kwargs
|
112
|
-
test_function = create_test(
|
113
|
-
operation=operation,
|
114
|
-
test=test,
|
115
|
-
settings=settings,
|
116
|
-
seed=seed,
|
117
|
-
data_generation_methods=operation.schema.data_generation_methods,
|
118
|
-
generation_config=generation_config,
|
119
|
-
as_strategy_kwargs=_as_strategy_kwargs,
|
120
|
-
)
|
121
|
-
yield Ok((operation, test_function))
|
122
|
-
|
123
|
-
|
124
|
-
def run_state_machine_as_test(
|
125
|
-
state_machine_factory: type[APIStateMachine], *, settings: hypothesis.settings | None = None
|
126
|
-
) -> None:
|
127
|
-
"""Run a state machine as a test.
|
128
|
-
|
129
|
-
It automatically adds the `_min_steps` argument if ``Hypothesis`` is recent enough.
|
130
|
-
"""
|
131
|
-
from hypothesis.stateful import run_state_machine_as_test as _run_state_machine_as_test
|
132
|
-
|
133
|
-
return _run_state_machine_as_test(state_machine_factory, settings=settings, _min_steps=2)
|
schemathesis/targets.py
DELETED
@@ -1,45 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
from dataclasses import dataclass
|
3
|
-
from typing import TYPE_CHECKING, Callable
|
4
|
-
|
5
|
-
if TYPE_CHECKING:
|
6
|
-
from .models import Case
|
7
|
-
from .transports.responses import GenericResponse
|
8
|
-
|
9
|
-
|
10
|
-
@dataclass
|
11
|
-
class TargetContext:
|
12
|
-
"""Context for targeted testing.
|
13
|
-
|
14
|
-
:ivar Case case: Generated example that is being processed.
|
15
|
-
:ivar GenericResponse response: API response.
|
16
|
-
:ivar float response_time: API response time.
|
17
|
-
"""
|
18
|
-
|
19
|
-
case: Case
|
20
|
-
response: GenericResponse
|
21
|
-
response_time: float
|
22
|
-
|
23
|
-
|
24
|
-
def response_time(context: TargetContext) -> float:
|
25
|
-
return context.response_time
|
26
|
-
|
27
|
-
|
28
|
-
Target = Callable[[TargetContext], float]
|
29
|
-
DEFAULT_TARGETS = ()
|
30
|
-
OPTIONAL_TARGETS = (response_time,)
|
31
|
-
ALL_TARGETS: tuple[Target, ...] = DEFAULT_TARGETS + OPTIONAL_TARGETS
|
32
|
-
|
33
|
-
|
34
|
-
def register(target: Target) -> Target:
|
35
|
-
"""Register a new testing target for schemathesis CLI.
|
36
|
-
|
37
|
-
:param target: A function that will be called to calculate a metric passed to ``hypothesis.target``.
|
38
|
-
"""
|
39
|
-
from . import cli
|
40
|
-
|
41
|
-
global ALL_TARGETS
|
42
|
-
|
43
|
-
ALL_TARGETS += (target,)
|
44
|
-
cli.TARGETS_TYPE.choices += (target.__name__,) # type: ignore
|
45
|
-
return target
|
schemathesis/throttling.py
DELETED
@@ -1,41 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
from typing import TYPE_CHECKING
|
3
|
-
|
4
|
-
from .exceptions import UsageError
|
5
|
-
|
6
|
-
|
7
|
-
if TYPE_CHECKING:
|
8
|
-
from pyrate_limiter import Limiter
|
9
|
-
|
10
|
-
|
11
|
-
def parse_units(rate: str) -> tuple[int, int]:
|
12
|
-
from pyrate_limiter import Duration
|
13
|
-
|
14
|
-
try:
|
15
|
-
limit, interval_text = rate.split("/")
|
16
|
-
interval = {
|
17
|
-
"s": Duration.SECOND,
|
18
|
-
"m": Duration.MINUTE,
|
19
|
-
"h": Duration.HOUR,
|
20
|
-
"d": Duration.DAY,
|
21
|
-
}.get(interval_text)
|
22
|
-
if interval is None:
|
23
|
-
raise invalid_rate(rate)
|
24
|
-
return int(limit), interval
|
25
|
-
except ValueError as exc:
|
26
|
-
raise invalid_rate(rate) from exc
|
27
|
-
|
28
|
-
|
29
|
-
def invalid_rate(value: str) -> UsageError:
|
30
|
-
return UsageError(
|
31
|
-
f"Invalid rate limit value: `{value}`. Should be in form `limit/interval`. "
|
32
|
-
"Example: `10/m` for 10 requests per minute."
|
33
|
-
)
|
34
|
-
|
35
|
-
|
36
|
-
def build_limiter(rate: str) -> Limiter:
|
37
|
-
from pyrate_limiter import Limiter, RequestRate
|
38
|
-
|
39
|
-
limit, interval = parse_units(rate)
|
40
|
-
rate = RequestRate(limit, interval)
|
41
|
-
return Limiter(rate)
|
schemathesis/transports/auth.py
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
from typing import TYPE_CHECKING
|
3
|
-
|
4
|
-
from ..types import RawAuth
|
5
|
-
|
6
|
-
if TYPE_CHECKING:
|
7
|
-
from requests.auth import HTTPDigestAuth
|
8
|
-
|
9
|
-
|
10
|
-
def get_requests_auth(auth: RawAuth | None, auth_type: str | None) -> HTTPDigestAuth | RawAuth | None:
|
11
|
-
from requests.auth import HTTPDigestAuth
|
12
|
-
|
13
|
-
if auth and auth_type == "digest":
|
14
|
-
return HTTPDigestAuth(*auth)
|
15
|
-
return auth
|
@@ -1,35 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
import re
|
3
|
-
from typing import Any
|
4
|
-
|
5
|
-
from ..constants import USER_AGENT
|
6
|
-
|
7
|
-
|
8
|
-
def setup_default_headers(kwargs: dict[str, Any]) -> None:
|
9
|
-
headers = kwargs.setdefault("headers", {})
|
10
|
-
if "user-agent" not in {header.lower() for header in headers}:
|
11
|
-
kwargs["headers"]["User-Agent"] = USER_AGENT
|
12
|
-
|
13
|
-
|
14
|
-
def is_latin_1_encodable(value: str) -> bool:
|
15
|
-
"""Header values are encoded to latin-1 before sending."""
|
16
|
-
try:
|
17
|
-
value.encode("latin-1")
|
18
|
-
return True
|
19
|
-
except UnicodeEncodeError:
|
20
|
-
return False
|
21
|
-
|
22
|
-
|
23
|
-
# Adapted from http.client._is_illegal_header_value
|
24
|
-
INVALID_HEADER_RE = re.compile(r"\n(?![ \t])|\r(?![ \t\n])")
|
25
|
-
|
26
|
-
|
27
|
-
def has_invalid_characters(name: str, value: str) -> bool:
|
28
|
-
from requests.utils import check_header_validity
|
29
|
-
from requests.exceptions import InvalidHeader
|
30
|
-
|
31
|
-
try:
|
32
|
-
check_header_validity((name, value))
|
33
|
-
return bool(INVALID_HEADER_RE.search(value))
|
34
|
-
except InvalidHeader:
|
35
|
-
return True
|
@@ -1,52 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import sys
|
4
|
-
import json
|
5
|
-
from typing import Union, TYPE_CHECKING, NoReturn, Any
|
6
|
-
from .._compat import JSONMixin
|
7
|
-
from werkzeug.wrappers import Response as BaseResponse
|
8
|
-
|
9
|
-
if TYPE_CHECKING:
|
10
|
-
from httpx import Response as httpxResponse
|
11
|
-
from requests import Response as requestsResponse
|
12
|
-
from requests import PreparedRequest
|
13
|
-
|
14
|
-
|
15
|
-
class WSGIResponse(BaseResponse, JSONMixin):
|
16
|
-
# We store "requests" request to build a reproduction code
|
17
|
-
request: PreparedRequest
|
18
|
-
|
19
|
-
def on_json_loading_failed(self, e: json.JSONDecodeError) -> NoReturn:
|
20
|
-
# We don't need a werkzeug-specific exception when JSON parsing error happens
|
21
|
-
raise e
|
22
|
-
|
23
|
-
|
24
|
-
def get_payload(response: GenericResponse) -> str:
|
25
|
-
from httpx import Response as httpxResponse
|
26
|
-
from requests import Response as requestsResponse
|
27
|
-
|
28
|
-
if isinstance(response, (httpxResponse, requestsResponse)):
|
29
|
-
return response.text
|
30
|
-
return response.get_data(as_text=True)
|
31
|
-
|
32
|
-
|
33
|
-
def get_json(response: GenericResponse) -> Any:
|
34
|
-
from httpx import Response as httpxResponse
|
35
|
-
from requests import Response as requestsResponse
|
36
|
-
|
37
|
-
if isinstance(response, (httpxResponse, requestsResponse)):
|
38
|
-
return json.loads(response.text)
|
39
|
-
return response.json
|
40
|
-
|
41
|
-
|
42
|
-
def get_reason(status_code: int) -> str:
|
43
|
-
if sys.version_info < (3, 9) and status_code == 418:
|
44
|
-
# Python 3.8 does not have 418 status in the `HTTPStatus` enum
|
45
|
-
return "I'm a Teapot"
|
46
|
-
|
47
|
-
import http.client
|
48
|
-
|
49
|
-
return http.client.responses.get(status_code, "Unknown")
|
50
|
-
|
51
|
-
|
52
|
-
GenericResponse = Union["httpxResponse", "requestsResponse", WSGIResponse]
|
schemathesis/types.py
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
from pathlib import Path
|
2
|
-
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Set, Tuple, Union
|
3
|
-
|
4
|
-
if TYPE_CHECKING:
|
5
|
-
from hypothesis.strategies import SearchStrategy
|
6
|
-
from .hooks import HookContext
|
7
|
-
|
8
|
-
PathLike = Union[Path, str]
|
9
|
-
|
10
|
-
Query = Dict[str, Any]
|
11
|
-
# Body can be of any Python type that corresponds to JSON Schema types + `bytes`
|
12
|
-
Body = Union[List, Dict[str, Any], str, int, float, bool, bytes]
|
13
|
-
PathParameters = Dict[str, Any]
|
14
|
-
Headers = Dict[str, Any]
|
15
|
-
Cookies = Dict[str, Any]
|
16
|
-
FormData = Dict[str, Any]
|
17
|
-
|
18
|
-
|
19
|
-
class NotSet:
|
20
|
-
pass
|
21
|
-
|
22
|
-
|
23
|
-
RequestCert = Union[str, Tuple[str, str]]
|
24
|
-
|
25
|
-
|
26
|
-
# A filter for path / method
|
27
|
-
Filter = Union[str, List[str], Tuple[str], Set[str], NotSet]
|
28
|
-
|
29
|
-
Hook = Union[
|
30
|
-
Callable[["SearchStrategy"], "SearchStrategy"], Callable[["SearchStrategy", "HookContext"], "SearchStrategy"]
|
31
|
-
]
|
32
|
-
|
33
|
-
RawAuth = Tuple[str, str]
|
34
|
-
# Generic test with any arguments and no return
|
35
|
-
GenericTest = Callable[..., None]
|
schemathesis/utils.py
DELETED
@@ -1,169 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
import functools
|
3
|
-
import operator
|
4
|
-
from contextlib import contextmanager
|
5
|
-
from inspect import getfullargspec
|
6
|
-
from pathlib import Path
|
7
|
-
from typing import (
|
8
|
-
Any,
|
9
|
-
Callable,
|
10
|
-
Generator,
|
11
|
-
NoReturn,
|
12
|
-
Union,
|
13
|
-
)
|
14
|
-
|
15
|
-
import pytest
|
16
|
-
from hypothesis import strategies as st
|
17
|
-
from hypothesis.core import is_invalid_test
|
18
|
-
from hypothesis.reporting import with_reporter
|
19
|
-
from hypothesis.strategies import SearchStrategy
|
20
|
-
|
21
|
-
from ._compat import InferType, get_signature
|
22
|
-
|
23
|
-
# Backward-compat
|
24
|
-
from .constants import NOT_SET # noqa: F401
|
25
|
-
from .exceptions import SkipTest, UsageError
|
26
|
-
from .types import GenericTest, PathLike
|
27
|
-
|
28
|
-
|
29
|
-
def is_schemathesis_test(func: Callable) -> bool:
|
30
|
-
"""Check whether test is parametrized with schemathesis."""
|
31
|
-
try:
|
32
|
-
from .schemas import BaseSchema
|
33
|
-
|
34
|
-
item = getattr(func, PARAMETRIZE_MARKER, None)
|
35
|
-
# Comparison is needed to avoid false-positives when mocks are collected by pytest
|
36
|
-
return isinstance(item, BaseSchema)
|
37
|
-
except Exception:
|
38
|
-
return False
|
39
|
-
|
40
|
-
|
41
|
-
def fail_on_no_matches(node_id: str) -> NoReturn: # type: ignore
|
42
|
-
pytest.fail(f"Test function {node_id} does not match any API operations and therefore has no effect")
|
43
|
-
|
44
|
-
|
45
|
-
IGNORED_PATTERNS = (
|
46
|
-
"Falsifying example: ",
|
47
|
-
"Falsifying explicit example: ",
|
48
|
-
"You can add @seed",
|
49
|
-
"Failed to reproduce exception. Expected:",
|
50
|
-
"Flaky example!",
|
51
|
-
"Traceback (most recent call last):",
|
52
|
-
"You can reproduce this example by temporarily",
|
53
|
-
"Unreliable test timings",
|
54
|
-
)
|
55
|
-
|
56
|
-
|
57
|
-
@contextmanager
|
58
|
-
def capture_hypothesis_output() -> Generator[list[str], None, None]:
|
59
|
-
"""Capture all output of Hypothesis into a list of strings.
|
60
|
-
|
61
|
-
It allows us to have more granular control over Schemathesis output.
|
62
|
-
|
63
|
-
Usage::
|
64
|
-
|
65
|
-
@given(i=st.integers())
|
66
|
-
def test(i):
|
67
|
-
assert 0
|
68
|
-
|
69
|
-
with capture_hypothesis_output() as output:
|
70
|
-
test() # hypothesis test
|
71
|
-
# output == ["Falsifying example: test(i=0)"]
|
72
|
-
"""
|
73
|
-
output = []
|
74
|
-
|
75
|
-
def get_output(value: str) -> None:
|
76
|
-
# Drop messages that could be confusing in the Schemathesis context
|
77
|
-
if value.startswith(IGNORED_PATTERNS):
|
78
|
-
return
|
79
|
-
output.append(value)
|
80
|
-
|
81
|
-
# the following context manager is untyped
|
82
|
-
with with_reporter(get_output): # type: ignore
|
83
|
-
yield output
|
84
|
-
|
85
|
-
|
86
|
-
GivenInput = Union[SearchStrategy, InferType]
|
87
|
-
PARAMETRIZE_MARKER = "_schemathesis_test"
|
88
|
-
GIVEN_ARGS_MARKER = "_schemathesis_given_args"
|
89
|
-
GIVEN_KWARGS_MARKER = "_schemathesis_given_kwargs"
|
90
|
-
|
91
|
-
|
92
|
-
def get_given_args(func: GenericTest) -> tuple:
|
93
|
-
return getattr(func, GIVEN_ARGS_MARKER, ())
|
94
|
-
|
95
|
-
|
96
|
-
def get_given_kwargs(func: GenericTest) -> dict[str, Any]:
|
97
|
-
return getattr(func, GIVEN_KWARGS_MARKER, {})
|
98
|
-
|
99
|
-
|
100
|
-
def is_given_applied(func: GenericTest) -> bool:
|
101
|
-
return hasattr(func, GIVEN_ARGS_MARKER) or hasattr(func, GIVEN_KWARGS_MARKER)
|
102
|
-
|
103
|
-
|
104
|
-
def given_proxy(*args: GivenInput, **kwargs: GivenInput) -> Callable[[GenericTest], GenericTest]:
|
105
|
-
"""Proxy Hypothesis strategies to ``hypothesis.given``."""
|
106
|
-
|
107
|
-
def wrapper(func: GenericTest) -> GenericTest:
|
108
|
-
if hasattr(func, GIVEN_ARGS_MARKER):
|
109
|
-
|
110
|
-
def wrapped_test(*_: Any, **__: Any) -> NoReturn:
|
111
|
-
raise UsageError(
|
112
|
-
f"You have applied `given` to the `{func.__name__}` test more than once, which "
|
113
|
-
"overrides the previous decorator. You need to pass all arguments to the same `given` call."
|
114
|
-
)
|
115
|
-
|
116
|
-
return wrapped_test
|
117
|
-
|
118
|
-
setattr(func, GIVEN_ARGS_MARKER, args)
|
119
|
-
setattr(func, GIVEN_KWARGS_MARKER, kwargs)
|
120
|
-
return func
|
121
|
-
|
122
|
-
return wrapper
|
123
|
-
|
124
|
-
|
125
|
-
def merge_given_args(func: GenericTest, args: tuple, kwargs: dict[str, Any]) -> dict[str, Any]:
|
126
|
-
"""Merge positional arguments to ``@schema.given`` into a dictionary with keyword arguments.
|
127
|
-
|
128
|
-
Kwargs are modified inplace.
|
129
|
-
"""
|
130
|
-
if args:
|
131
|
-
argspec = getfullargspec(func)
|
132
|
-
for name, strategy in zip(reversed([arg for arg in argspec.args if arg != "case"]), reversed(args)):
|
133
|
-
kwargs[name] = strategy
|
134
|
-
return kwargs
|
135
|
-
|
136
|
-
|
137
|
-
def validate_given_args(func: GenericTest, args: tuple, kwargs: dict[str, Any]) -> Callable | None:
|
138
|
-
signature = get_signature(func)
|
139
|
-
return is_invalid_test(func, signature, args, kwargs) # type: ignore
|
140
|
-
|
141
|
-
|
142
|
-
def compose(*functions: Callable) -> Callable:
|
143
|
-
"""Compose multiple functions into a single one."""
|
144
|
-
|
145
|
-
def noop(x: Any) -> Any:
|
146
|
-
return x
|
147
|
-
|
148
|
-
return functools.reduce(lambda f, g: lambda x: f(g(x)), functions, noop)
|
149
|
-
|
150
|
-
|
151
|
-
def combine_strategies(strategies: list[st.SearchStrategy]) -> st.SearchStrategy:
|
152
|
-
"""Combine a list of strategies into a single one.
|
153
|
-
|
154
|
-
If the input is `[a, b, c]`, then the result is equivalent to `a | b | c`.
|
155
|
-
"""
|
156
|
-
return functools.reduce(operator.or_, strategies[1:], strategies[0])
|
157
|
-
|
158
|
-
|
159
|
-
def skip(operation_name: str) -> NoReturn:
|
160
|
-
raise SkipTest(f"It is not possible to generate negative test cases for `{operation_name}`")
|
161
|
-
|
162
|
-
|
163
|
-
def _ensure_parent(path: PathLike, fail_silently: bool = True) -> None:
|
164
|
-
# Try to create the parent dir
|
165
|
-
try:
|
166
|
-
Path(path).parent.mkdir(mode=0o755, parents=True, exist_ok=True)
|
167
|
-
except OSError:
|
168
|
-
if not fail_silently:
|
169
|
-
raise
|