schemathesis 3.39.15__py3-none-any.whl → 4.0.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 +41 -79
- schemathesis/auths.py +111 -122
- schemathesis/checks.py +169 -60
- schemathesis/cli/__init__.py +15 -2117
- schemathesis/cli/commands/__init__.py +85 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +590 -0
- schemathesis/cli/commands/run/context.py +204 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -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 +474 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
- schemathesis/cli/commands/run/handlers/output.py +1628 -0
- schemathesis/cli/commands/run/loaders.py +114 -0
- schemathesis/cli/commands/run/validation.py +246 -0
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +84 -0
- schemathesis/cli/{options.py → ext/options.py} +36 -34
- schemathesis/config/__init__.py +189 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +99 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +156 -0
- schemathesis/config/_generation.py +149 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +327 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +187 -0
- schemathesis/config/_projects.py +527 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +885 -0
- schemathesis/core/__init__.py +67 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +459 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/{throttling.py → core/rate_limit.py} +16 -17
- schemathesis/core/registries.py +31 -0
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +54 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +118 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +169 -0
- schemathesis/engine/errors.py +464 -0
- schemathesis/engine/events.py +258 -0
- schemathesis/engine/phases/__init__.py +88 -0
- schemathesis/{runner → engine/phases}/probes.py +52 -68
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +356 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +212 -0
- schemathesis/engine/phases/unit/_executor.py +416 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +247 -0
- schemathesis/errors.py +43 -0
- schemathesis/filters.py +17 -98
- schemathesis/generation/__init__.py +5 -33
- schemathesis/generation/case.py +317 -0
- schemathesis/generation/coverage.py +282 -175
- schemathesis/generation/hypothesis/__init__.py +36 -0
- schemathesis/generation/hypothesis/builder.py +800 -0
- schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
- 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/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +116 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +278 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +284 -0
- schemathesis/hooks.py +80 -101
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +455 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +313 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +281 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -273
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +42 -6
- schemathesis/specs/graphql/schemas.py +141 -137
- schemathesis/specs/graphql/validation.py +11 -17
- schemathesis/specs/openapi/__init__.py +6 -1
- schemathesis/specs/openapi/_cache.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +142 -156
- schemathesis/specs/openapi/checks.py +368 -257
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +23 -21
- schemathesis/specs/openapi/expressions/__init__.py +31 -19
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/lexer.py +1 -1
- schemathesis/specs/openapi/expressions/nodes.py +36 -41
- schemathesis/specs/openapi/expressions/parser.py +1 -1
- schemathesis/specs/openapi/formats.py +35 -7
- schemathesis/specs/openapi/media_types.py +53 -12
- schemathesis/specs/openapi/negative/__init__.py +7 -4
- schemathesis/specs/openapi/negative/mutations.py +6 -5
- schemathesis/specs/openapi/parameters.py +7 -10
- schemathesis/specs/openapi/patterns.py +94 -31
- schemathesis/specs/openapi/references.py +12 -53
- schemathesis/specs/openapi/schemas.py +238 -308
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +12 -6
- schemathesis/specs/openapi/stateful/__init__.py +268 -133
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/links.py +209 -0
- schemathesis/transport/__init__.py +142 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +124 -0
- schemathesis/transport/requests.py +244 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -11
- schemathesis/transport/wsgi.py +171 -0
- schemathesis-4.0.0.dist-info/METADATA +204 -0
- schemathesis-4.0.0.dist-info/RECORD +164 -0
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -712
- schemathesis/_override.py +0 -50
- schemathesis/_patches.py +0 -21
- schemathesis/_rate_limiter.py +0 -7
- schemathesis/cli/callbacks.py +0 -466
- schemathesis/cli/cassettes.py +0 -561
- schemathesis/cli/context.py +0 -75
- schemathesis/cli/debug.py +0 -27
- schemathesis/cli/handlers.py +0 -19
- schemathesis/cli/junitxml.py +0 -124
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -920
- schemathesis/cli/output/short.py +0 -59
- schemathesis/cli/reporting.py +0 -79
- schemathesis/cli/sanitization.py +0 -26
- schemathesis/code_samples.py +0 -151
- schemathesis/constants.py +0 -54
- schemathesis/contrib/__init__.py +0 -11
- schemathesis/contrib/openapi/__init__.py +0 -11
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -16
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -571
- schemathesis/experimental/__init__.py +0 -109
- schemathesis/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -284
- schemathesis/fixups/__init__.py +0 -37
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -28
- schemathesis/generation/_methods.py +0 -44
- schemathesis/graphql.py +0 -3
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/checks.py +0 -86
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -37
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- schemathesis/internal/output.py +0 -68
- schemathesis/internal/transformation.py +0 -26
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -474
- schemathesis/loaders.py +0 -122
- schemathesis/models.py +0 -1341
- schemathesis/parameters.py +0 -90
- schemathesis/runner/__init__.py +0 -605
- schemathesis/runner/events.py +0 -389
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/context.py +0 -88
- schemathesis/runner/impl/core.py +0 -1280
- schemathesis/runner/impl/solo.py +0 -80
- schemathesis/runner/impl/threadpool.py +0 -391
- schemathesis/runner/serialization.py +0 -544
- schemathesis/sanitization.py +0 -252
- schemathesis/serializers.py +0 -328
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -202
- schemathesis/service/client.py +0 -133
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -61
- schemathesis/service/extensions.py +0 -224
- schemathesis/service/hosts.py +0 -111
- schemathesis/service/metadata.py +0 -71
- schemathesis/service/models.py +0 -258
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -173
- schemathesis/service/usage.py +0 -66
- schemathesis/specs/graphql/loaders.py +0 -364
- schemathesis/specs/openapi/expressions/context.py +0 -16
- schemathesis/specs/openapi/links.py +0 -389
- schemathesis/specs/openapi/loaders.py +0 -707
- schemathesis/specs/openapi/stateful/statistic.py +0 -198
- schemathesis/specs/openapi/stateful/types.py +0 -14
- schemathesis/specs/openapi/validation.py +0 -26
- schemathesis/stateful/__init__.py +0 -147
- schemathesis/stateful/config.py +0 -97
- schemathesis/stateful/context.py +0 -135
- schemathesis/stateful/events.py +0 -274
- schemathesis/stateful/runner.py +0 -309
- schemathesis/stateful/sink.py +0 -68
- schemathesis/stateful/state_machine.py +0 -328
- schemathesis/stateful/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -369
- schemathesis/transports/asgi.py +0 -7
- schemathesis/transports/auth.py +0 -38
- schemathesis/transports/headers.py +0 -36
- schemathesis/transports/responses.py +0 -57
- schemathesis/types.py +0 -44
- schemathesis/utils.py +0 -164
- schemathesis-3.39.15.dist-info/METADATA +0 -293
- schemathesis-3.39.15.dist-info/RECORD +0 -160
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
- /schemathesis/{internal → core}/result.py +0 -0
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -1,8 +1,7 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import os
|
4
|
-
from functools import lru_cache
|
5
|
-
from operator import or_
|
4
|
+
from functools import lru_cache
|
6
5
|
from typing import TYPE_CHECKING, TypeVar
|
7
6
|
|
8
7
|
if TYPE_CHECKING:
|
@@ -29,7 +28,7 @@ def default_settings() -> settings:
|
|
29
28
|
T = TypeVar("T")
|
30
29
|
|
31
30
|
|
32
|
-
def
|
31
|
+
def generate_one(strategy: st.SearchStrategy[T]) -> T: # type: ignore[type-var]
|
33
32
|
examples: list[T] = []
|
34
33
|
add_single_example(strategy, examples)
|
35
34
|
return examples[0]
|
@@ -49,11 +48,3 @@ def add_single_example(strategy: st.SearchStrategy[T], examples: list[T]) -> Non
|
|
49
48
|
example_generating_inner_function = seed(SCHEMATHESIS_BENCHMARK_SEED)(example_generating_inner_function)
|
50
49
|
|
51
50
|
example_generating_inner_function()
|
52
|
-
|
53
|
-
|
54
|
-
def combine_strategies(strategies: list[st.SearchStrategy] | tuple[st.SearchStrategy]) -> st.SearchStrategy:
|
55
|
-
"""Combine a list of strategies into a single one.
|
56
|
-
|
57
|
-
If the input is `[a, b, c]`, then the result is equivalent to `a | b | c`.
|
58
|
-
"""
|
59
|
-
return reduce(or_, strategies[1:], strategies[0])
|
@@ -0,0 +1,66 @@
|
|
1
|
+
"""Integrating `hypothesis.given` into Schemathesis."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from inspect import getfullargspec
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable, NoReturn, Union
|
7
|
+
|
8
|
+
from schemathesis.core.errors import IncorrectUsage
|
9
|
+
from schemathesis.core.marks import Mark
|
10
|
+
|
11
|
+
if TYPE_CHECKING:
|
12
|
+
from hypothesis.strategies import SearchStrategy
|
13
|
+
|
14
|
+
|
15
|
+
__all__ = ["is_given_applied", "given_proxy", "merge_given_args", "GivenInput", "GivenArgsMark", "GivenKwargsMark"]
|
16
|
+
|
17
|
+
EllipsisType = type(...)
|
18
|
+
GivenInput = Union["SearchStrategy", EllipsisType] # type: ignore[valid-type]
|
19
|
+
|
20
|
+
GivenArgsMark = Mark[tuple](attr_name="given_args", default=())
|
21
|
+
GivenKwargsMark = Mark[dict[str, Any]](attr_name="given_kwargs", default=dict)
|
22
|
+
|
23
|
+
|
24
|
+
def is_given_applied(func: Callable) -> bool:
|
25
|
+
return GivenArgsMark.is_set(func) or GivenKwargsMark.is_set(func)
|
26
|
+
|
27
|
+
|
28
|
+
def given_proxy(*args: GivenInput, **kwargs: GivenInput) -> Callable[[Callable], Callable]:
|
29
|
+
"""Proxy Hypothesis strategies to ``hypothesis.given``."""
|
30
|
+
|
31
|
+
def wrapper(func: Callable) -> Callable:
|
32
|
+
if is_given_applied(func):
|
33
|
+
|
34
|
+
def wrapped_test(*_: Any, **__: Any) -> NoReturn:
|
35
|
+
raise IncorrectUsage(
|
36
|
+
f"You have applied `given` to the `{func.__name__}` test more than once, which "
|
37
|
+
"overrides the previous decorator. You need to pass all arguments to the same `given` call."
|
38
|
+
)
|
39
|
+
|
40
|
+
return wrapped_test
|
41
|
+
|
42
|
+
GivenArgsMark.set(func, args)
|
43
|
+
GivenKwargsMark.set(func, kwargs)
|
44
|
+
return func
|
45
|
+
|
46
|
+
return wrapper
|
47
|
+
|
48
|
+
|
49
|
+
def merge_given_args(func: Callable, args: tuple, kwargs: dict[str, Any]) -> dict[str, Any]:
|
50
|
+
"""Merge positional arguments to ``@schema.given`` into a dictionary with keyword arguments.
|
51
|
+
|
52
|
+
Kwargs are modified inplace.
|
53
|
+
"""
|
54
|
+
if args:
|
55
|
+
argspec = getfullargspec(func)
|
56
|
+
for name, strategy in zip(reversed([arg for arg in argspec.args if arg != "case"]), reversed(args)):
|
57
|
+
kwargs[name] = strategy
|
58
|
+
return kwargs
|
59
|
+
|
60
|
+
|
61
|
+
def validate_given_args(func: Callable, args: tuple, kwargs: dict[str, Any]) -> Callable | None:
|
62
|
+
from hypothesis.core import is_invalid_test
|
63
|
+
from hypothesis.internal.reflection import get_signature
|
64
|
+
|
65
|
+
signature = get_signature(func)
|
66
|
+
return is_invalid_test(func, signature, args, kwargs) # type: ignore
|
@@ -0,0 +1,14 @@
|
|
1
|
+
from contextlib import contextmanager
|
2
|
+
from typing import Generator
|
3
|
+
|
4
|
+
from hypothesis.reporting import with_reporter
|
5
|
+
|
6
|
+
|
7
|
+
def ignore(_: str) -> None:
|
8
|
+
pass
|
9
|
+
|
10
|
+
|
11
|
+
@contextmanager
|
12
|
+
def ignore_hypothesis_output() -> Generator:
|
13
|
+
with with_reporter(ignore): # type: ignore
|
14
|
+
yield
|
@@ -0,0 +1,16 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from functools import reduce
|
4
|
+
from operator import or_
|
5
|
+
from typing import TYPE_CHECKING
|
6
|
+
|
7
|
+
if TYPE_CHECKING:
|
8
|
+
from hypothesis import strategies as st
|
9
|
+
|
10
|
+
|
11
|
+
def combine(strategies: list[st.SearchStrategy] | tuple[st.SearchStrategy]) -> st.SearchStrategy:
|
12
|
+
"""Combine a list of strategies into a single one.
|
13
|
+
|
14
|
+
If the input is `[a, b, c]`, then the result is equivalent to `a | b | c`.
|
15
|
+
"""
|
16
|
+
return reduce(or_, strategies[1:], strategies[0])
|
@@ -0,0 +1,115 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from enum import Enum
|
5
|
+
|
6
|
+
from schemathesis.generation import GenerationMode
|
7
|
+
|
8
|
+
|
9
|
+
class TestPhase(str, Enum):
|
10
|
+
__test__ = False
|
11
|
+
|
12
|
+
EXAMPLES = "examples"
|
13
|
+
COVERAGE = "coverage"
|
14
|
+
FUZZING = "fuzzing"
|
15
|
+
|
16
|
+
|
17
|
+
class ComponentKind(str, Enum):
|
18
|
+
"""Components that can be generated."""
|
19
|
+
|
20
|
+
QUERY = "query"
|
21
|
+
PATH_PARAMETERS = "path_parameters"
|
22
|
+
HEADERS = "headers"
|
23
|
+
COOKIES = "cookies"
|
24
|
+
BODY = "body"
|
25
|
+
|
26
|
+
|
27
|
+
@dataclass
|
28
|
+
class ComponentInfo:
|
29
|
+
"""Information about how a specific component was generated."""
|
30
|
+
|
31
|
+
mode: GenerationMode
|
32
|
+
|
33
|
+
__slots__ = ("mode",)
|
34
|
+
|
35
|
+
|
36
|
+
@dataclass
|
37
|
+
class GeneratePhaseData:
|
38
|
+
"""Metadata specific to generate phase."""
|
39
|
+
|
40
|
+
|
41
|
+
@dataclass
|
42
|
+
class ExplicitPhaseData:
|
43
|
+
"""Metadata specific to explicit phase."""
|
44
|
+
|
45
|
+
|
46
|
+
@dataclass
|
47
|
+
class CoveragePhaseData:
|
48
|
+
"""Metadata specific to coverage phase."""
|
49
|
+
|
50
|
+
description: str
|
51
|
+
location: str | None
|
52
|
+
parameter: str | None
|
53
|
+
parameter_location: str | None
|
54
|
+
|
55
|
+
__slots__ = ("description", "location", "parameter", "parameter_location")
|
56
|
+
|
57
|
+
|
58
|
+
@dataclass
|
59
|
+
class PhaseInfo:
|
60
|
+
"""Phase-specific information."""
|
61
|
+
|
62
|
+
name: TestPhase
|
63
|
+
data: CoveragePhaseData | ExplicitPhaseData | GeneratePhaseData
|
64
|
+
|
65
|
+
__slots__ = ("name", "data")
|
66
|
+
|
67
|
+
@classmethod
|
68
|
+
def coverage(
|
69
|
+
cls,
|
70
|
+
description: str,
|
71
|
+
location: str | None = None,
|
72
|
+
parameter: str | None = None,
|
73
|
+
parameter_location: str | None = None,
|
74
|
+
) -> PhaseInfo:
|
75
|
+
return cls(
|
76
|
+
name=TestPhase.COVERAGE,
|
77
|
+
data=CoveragePhaseData(
|
78
|
+
description=description, location=location, parameter=parameter, parameter_location=parameter_location
|
79
|
+
),
|
80
|
+
)
|
81
|
+
|
82
|
+
@classmethod
|
83
|
+
def generate(cls) -> PhaseInfo:
|
84
|
+
return cls(name=TestPhase.FUZZING, data=GeneratePhaseData())
|
85
|
+
|
86
|
+
|
87
|
+
@dataclass
|
88
|
+
class GenerationInfo:
|
89
|
+
"""Information about test case generation."""
|
90
|
+
|
91
|
+
time: float
|
92
|
+
mode: GenerationMode
|
93
|
+
|
94
|
+
__slots__ = ("time", "mode")
|
95
|
+
|
96
|
+
|
97
|
+
@dataclass
|
98
|
+
class CaseMetadata:
|
99
|
+
"""Complete metadata for generated cases."""
|
100
|
+
|
101
|
+
generation: GenerationInfo
|
102
|
+
components: dict[ComponentKind, ComponentInfo]
|
103
|
+
phase: PhaseInfo
|
104
|
+
|
105
|
+
__slots__ = ("generation", "components", "phase")
|
106
|
+
|
107
|
+
def __init__(
|
108
|
+
self,
|
109
|
+
generation: GenerationInfo,
|
110
|
+
components: dict[ComponentKind, ComponentInfo],
|
111
|
+
phase: PhaseInfo,
|
112
|
+
) -> None:
|
113
|
+
self.generation = generation
|
114
|
+
self.components = components
|
115
|
+
self.phase = phase
|
@@ -0,0 +1,93 @@
|
|
1
|
+
"""Support for Targeted Property-Based Testing."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from dataclasses import dataclass
|
6
|
+
from typing import Callable, Sequence
|
7
|
+
|
8
|
+
from schemathesis.core.registries import Registry
|
9
|
+
from schemathesis.core.transport import Response
|
10
|
+
from schemathesis.generation.case import Case
|
11
|
+
|
12
|
+
|
13
|
+
@dataclass
|
14
|
+
class MetricContext:
|
15
|
+
"""Context for evaluating a metric on a single test execution.
|
16
|
+
|
17
|
+
This object bundles together the test `case` that was sent and
|
18
|
+
the corresponding HTTP `response`. Metric functions receive an
|
19
|
+
instance of `MetricContext` to compute a numeric score.
|
20
|
+
"""
|
21
|
+
|
22
|
+
case: Case
|
23
|
+
"""Generated test case."""
|
24
|
+
response: Response
|
25
|
+
"""The HTTP response returned by the server for this test case."""
|
26
|
+
|
27
|
+
__slots__ = ("case", "response")
|
28
|
+
|
29
|
+
|
30
|
+
MetricFunction = Callable[[MetricContext], float]
|
31
|
+
|
32
|
+
METRICS = Registry[MetricFunction]()
|
33
|
+
|
34
|
+
|
35
|
+
def metric(func: MetricFunction) -> MetricFunction:
|
36
|
+
"""Decorator to register a custom metric for targeted property-based testing.
|
37
|
+
|
38
|
+
Example:
|
39
|
+
```python
|
40
|
+
import schemathesis
|
41
|
+
|
42
|
+
@schemathesis.metric
|
43
|
+
def response_size(ctx: schemathesis.MetricContext) -> float:
|
44
|
+
return float(len(ctx.response.content))
|
45
|
+
```
|
46
|
+
|
47
|
+
"""
|
48
|
+
return METRICS.register(func)
|
49
|
+
|
50
|
+
|
51
|
+
@metric
|
52
|
+
def response_time(ctx: MetricContext) -> float:
|
53
|
+
"""Response time as a metric to maximize."""
|
54
|
+
return ctx.response.elapsed
|
55
|
+
|
56
|
+
|
57
|
+
class MetricCollector:
|
58
|
+
"""Collect multiple observations for metrics."""
|
59
|
+
|
60
|
+
__slots__ = ("metrics", "observations")
|
61
|
+
|
62
|
+
def __init__(self, metrics: list[MetricFunction] | None = None) -> None:
|
63
|
+
self.metrics = metrics or []
|
64
|
+
self.observations: dict[str, list[float]] = {metric.__name__: [] for metric in self.metrics}
|
65
|
+
|
66
|
+
def reset(self) -> None:
|
67
|
+
"""Reset all collected observations."""
|
68
|
+
for metric in self.metrics:
|
69
|
+
self.observations[metric.__name__].clear()
|
70
|
+
|
71
|
+
def store(self, case: Case, response: Response) -> None:
|
72
|
+
"""Calculate metrics & store them."""
|
73
|
+
ctx = MetricContext(case=case, response=response)
|
74
|
+
for metric in self.metrics:
|
75
|
+
self.observations[metric.__name__].append(metric(ctx))
|
76
|
+
|
77
|
+
def maximize(self) -> None:
|
78
|
+
"""Give feedback to the Hypothesis engine, so it maximizes the aggregated metrics."""
|
79
|
+
import hypothesis
|
80
|
+
|
81
|
+
for metric in self.metrics:
|
82
|
+
# Currently aggregation is just a sum
|
83
|
+
value = sum(self.observations[metric.__name__])
|
84
|
+
hypothesis.target(value, label=metric.__name__)
|
85
|
+
|
86
|
+
|
87
|
+
def maximize(metrics: Sequence[MetricFunction], case: Case, response: Response) -> None:
|
88
|
+
import hypothesis
|
89
|
+
|
90
|
+
ctx = MetricContext(case=case, response=response)
|
91
|
+
for metric in metrics:
|
92
|
+
value = metric(ctx)
|
93
|
+
hypothesis.target(value, label=metric.__name__)
|
@@ -0,0 +1,20 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from enum import Enum
|
4
|
+
|
5
|
+
|
6
|
+
class GenerationMode(str, Enum):
|
7
|
+
"""Defines what data Schemathesis generates for tests."""
|
8
|
+
|
9
|
+
# Generate data, that fits the API schema
|
10
|
+
POSITIVE = "positive"
|
11
|
+
# Doesn't fit the API schema
|
12
|
+
NEGATIVE = "negative"
|
13
|
+
|
14
|
+
@property
|
15
|
+
def is_positive(self) -> bool:
|
16
|
+
return self == GenerationMode.POSITIVE
|
17
|
+
|
18
|
+
@property
|
19
|
+
def is_negative(self) -> bool:
|
20
|
+
return self == GenerationMode.NEGATIVE
|
@@ -0,0 +1,116 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from collections.abc import Mapping
|
4
|
+
from dataclasses import dataclass
|
5
|
+
from typing import TYPE_CHECKING, Any, Iterator
|
6
|
+
|
7
|
+
from schemathesis.config import ProjectConfig
|
8
|
+
from schemathesis.core.transforms import diff
|
9
|
+
from schemathesis.generation.meta import ComponentKind
|
10
|
+
|
11
|
+
if TYPE_CHECKING:
|
12
|
+
from schemathesis.generation.case import Case
|
13
|
+
from schemathesis.schemas import APIOperation, Parameter
|
14
|
+
|
15
|
+
|
16
|
+
@dataclass
|
17
|
+
class Override:
|
18
|
+
"""Overrides for various parts of a test case."""
|
19
|
+
|
20
|
+
query: dict[str, str]
|
21
|
+
headers: dict[str, str]
|
22
|
+
cookies: dict[str, str]
|
23
|
+
path_parameters: dict[str, str]
|
24
|
+
|
25
|
+
def items(self) -> Iterator[tuple[str, dict[str, str]]]:
|
26
|
+
for key, value in (
|
27
|
+
("query", self.query),
|
28
|
+
("headers", self.headers),
|
29
|
+
("cookies", self.cookies),
|
30
|
+
("path_parameters", self.path_parameters),
|
31
|
+
):
|
32
|
+
if value:
|
33
|
+
yield key, value
|
34
|
+
|
35
|
+
@classmethod
|
36
|
+
def from_components(cls, components: dict[ComponentKind, StoredValue], case: Case) -> Override:
|
37
|
+
return Override(
|
38
|
+
**{
|
39
|
+
kind.value: get_component_diff(stored=stored, current=getattr(case, kind.value))
|
40
|
+
for kind, stored in components.items()
|
41
|
+
}
|
42
|
+
)
|
43
|
+
|
44
|
+
|
45
|
+
def for_operation(config: ProjectConfig, *, operation: APIOperation) -> Override:
|
46
|
+
operation_config = config.operations.get_for_operation(operation)
|
47
|
+
|
48
|
+
output = Override(query={}, headers={}, cookies={}, path_parameters={})
|
49
|
+
groups = [
|
50
|
+
(output.query, operation.query),
|
51
|
+
(output.headers, operation.headers),
|
52
|
+
(output.cookies, operation.cookies),
|
53
|
+
(output.path_parameters, operation.path_parameters),
|
54
|
+
]
|
55
|
+
for container, params in groups:
|
56
|
+
for param in params:
|
57
|
+
# Attempt to get the override from the operation-specific configuration.
|
58
|
+
value = None
|
59
|
+
if operation_config:
|
60
|
+
value = _get_override_value(param, operation_config.parameters)
|
61
|
+
# Fallback to the global project configuration.
|
62
|
+
if value is None:
|
63
|
+
value = _get_override_value(param, config.parameters)
|
64
|
+
if value is not None:
|
65
|
+
container[param.name] = value
|
66
|
+
|
67
|
+
return output
|
68
|
+
|
69
|
+
|
70
|
+
def _get_override_value(param: Parameter, parameters: dict[str, Any]) -> Any:
|
71
|
+
key = param.name
|
72
|
+
full_key = f"{param.location}.{param.name}"
|
73
|
+
if key in parameters:
|
74
|
+
return parameters[key]
|
75
|
+
elif full_key in parameters:
|
76
|
+
return parameters[full_key]
|
77
|
+
return None
|
78
|
+
|
79
|
+
|
80
|
+
@dataclass
|
81
|
+
class StoredValue:
|
82
|
+
value: dict[str, Any] | None
|
83
|
+
is_generated: bool
|
84
|
+
|
85
|
+
__slots__ = ("value", "is_generated")
|
86
|
+
|
87
|
+
|
88
|
+
def store_original_state(value: dict[str, Any] | None) -> dict[str, Any] | None:
|
89
|
+
if isinstance(value, Mapping):
|
90
|
+
return value.copy()
|
91
|
+
return value
|
92
|
+
|
93
|
+
|
94
|
+
def get_component_diff(stored: StoredValue, current: dict[str, Any] | None) -> dict[str, Any]:
|
95
|
+
"""Calculate difference between stored and current components."""
|
96
|
+
if not (current and stored.value):
|
97
|
+
return {}
|
98
|
+
if stored.is_generated:
|
99
|
+
return diff(stored.value, current)
|
100
|
+
return current
|
101
|
+
|
102
|
+
|
103
|
+
def store_components(case: Case) -> dict[ComponentKind, StoredValue]:
|
104
|
+
"""Store original component states for a test case."""
|
105
|
+
return {
|
106
|
+
kind: StoredValue(
|
107
|
+
value=store_original_state(getattr(case, kind.value)),
|
108
|
+
is_generated=bool(case.meta and kind in case.meta.components),
|
109
|
+
)
|
110
|
+
for kind in [
|
111
|
+
ComponentKind.QUERY,
|
112
|
+
ComponentKind.HEADERS,
|
113
|
+
ComponentKind.COOKIES,
|
114
|
+
ComponentKind.PATH_PARAMETERS,
|
115
|
+
]
|
116
|
+
}
|
@@ -0,0 +1,37 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import TYPE_CHECKING
|
4
|
+
|
5
|
+
if TYPE_CHECKING:
|
6
|
+
import hypothesis
|
7
|
+
|
8
|
+
from schemathesis.generation.stateful.state_machine import APIStateMachine
|
9
|
+
|
10
|
+
__all__ = [
|
11
|
+
"APIStateMachine",
|
12
|
+
]
|
13
|
+
|
14
|
+
|
15
|
+
def __getattr__(name: str) -> type[APIStateMachine]:
|
16
|
+
if name == "APIStateMachine":
|
17
|
+
from schemathesis.generation.stateful.state_machine import APIStateMachine
|
18
|
+
|
19
|
+
return APIStateMachine
|
20
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
21
|
+
|
22
|
+
|
23
|
+
STATEFUL_TESTS_LABEL = "Stateful tests"
|
24
|
+
|
25
|
+
|
26
|
+
def run_state_machine_as_test(
|
27
|
+
state_machine_factory: type[APIStateMachine], *, settings: hypothesis.settings | None = None
|
28
|
+
) -> None:
|
29
|
+
"""Run a state machine as a test.
|
30
|
+
|
31
|
+
It automatically adds the `_min_steps` argument if ``Hypothesis`` is recent enough.
|
32
|
+
"""
|
33
|
+
from hypothesis.stateful import run_state_machine_as_test as _run_state_machine_as_test
|
34
|
+
|
35
|
+
__tracebackhide__ = True
|
36
|
+
|
37
|
+
return _run_state_machine_as_test(state_machine_factory, settings=settings, _min_steps=2)
|