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
@@ -0,0 +1,30 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
# Maximum test running time
|
4
|
+
DEFAULT_DEADLINE = 15000
|
5
|
+
|
6
|
+
|
7
|
+
def setup() -> None:
|
8
|
+
from hypothesis.internal.entropy import deterministic_PRNG
|
9
|
+
from hypothesis.internal.reflection import is_first_param_referenced_in_function
|
10
|
+
from hypothesis.strategies._internal import core
|
11
|
+
from hypothesis_jsonschema import _from_schema, _resolve
|
12
|
+
|
13
|
+
from schemathesis.core.transforms import deepclone
|
14
|
+
|
15
|
+
# Forcefully initializes Hypothesis' global PRNG to avoid races that initialize it
|
16
|
+
# if e.g. Schemathesis CLI is used with multiple workers
|
17
|
+
with deterministic_PRNG():
|
18
|
+
pass
|
19
|
+
|
20
|
+
# A set of performance-related patches
|
21
|
+
|
22
|
+
# This one is used a lot, and under the hood it re-parses the AST of the same function
|
23
|
+
def _is_first_param_referenced_in_function(f: Any) -> bool:
|
24
|
+
if f.__name__ == "from_object_schema" and f.__module__ == "hypothesis_jsonschema._from_schema":
|
25
|
+
return True
|
26
|
+
return is_first_param_referenced_in_function(f)
|
27
|
+
|
28
|
+
core.is_first_param_referenced_in_function = _is_first_param_referenced_in_function # type: ignore
|
29
|
+
_resolve.deepcopy = deepclone # type: ignore
|
30
|
+
_from_schema.deepcopy = deepclone # type: ignore
|
@@ -0,0 +1,585 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import json
|
4
|
+
from dataclasses import dataclass, field
|
5
|
+
from functools import wraps
|
6
|
+
from itertools import combinations
|
7
|
+
from time import perf_counter
|
8
|
+
from typing import Any, Callable, Generator, Mapping
|
9
|
+
|
10
|
+
import hypothesis
|
11
|
+
from hypothesis import Phase
|
12
|
+
from hypothesis import strategies as st
|
13
|
+
from hypothesis.errors import Unsatisfiable
|
14
|
+
from jsonschema.exceptions import SchemaError
|
15
|
+
|
16
|
+
from schemathesis.auths import AuthStorageMark
|
17
|
+
from schemathesis.core import NOT_SET, NotSet, media_types
|
18
|
+
from schemathesis.core.errors import InvalidSchema, SerializationNotPossible
|
19
|
+
from schemathesis.core.marks import Mark
|
20
|
+
from schemathesis.core.result import Ok, Result
|
21
|
+
from schemathesis.core.transport import prepare_urlencoded
|
22
|
+
from schemathesis.core.validation import has_invalid_characters, is_latin_1_encodable
|
23
|
+
from schemathesis.experimental import COVERAGE_PHASE
|
24
|
+
from schemathesis.generation import GenerationConfig, GenerationMode, coverage
|
25
|
+
from schemathesis.generation.case import Case
|
26
|
+
from schemathesis.generation.hypothesis import DEFAULT_DEADLINE, examples, setup, strategies
|
27
|
+
from schemathesis.generation.hypothesis.given import GivenInput
|
28
|
+
from schemathesis.generation.meta import CaseMetadata, CoveragePhaseData, GenerationInfo, PhaseInfo
|
29
|
+
from schemathesis.hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookDispatcherMark
|
30
|
+
from schemathesis.schemas import APIOperation, BaseSchema, ParameterSet
|
31
|
+
|
32
|
+
setup()
|
33
|
+
|
34
|
+
|
35
|
+
def get_all_tests(
|
36
|
+
*,
|
37
|
+
schema: BaseSchema,
|
38
|
+
test_func: Callable,
|
39
|
+
generation_config: GenerationConfig,
|
40
|
+
settings: hypothesis.settings | None = None,
|
41
|
+
seed: int | None = None,
|
42
|
+
as_strategy_kwargs: Callable[[APIOperation], dict[str, Any]] | None = None,
|
43
|
+
given_kwargs: dict[str, GivenInput] | None = None,
|
44
|
+
) -> Generator[Result[tuple[APIOperation, Callable], InvalidSchema], None, None]:
|
45
|
+
"""Generate all operations and Hypothesis tests for them."""
|
46
|
+
for result in schema.get_all_operations(generation_config=generation_config):
|
47
|
+
if isinstance(result, Ok):
|
48
|
+
operation = result.ok()
|
49
|
+
if callable(as_strategy_kwargs):
|
50
|
+
_as_strategy_kwargs = as_strategy_kwargs(operation)
|
51
|
+
else:
|
52
|
+
_as_strategy_kwargs = {}
|
53
|
+
test = create_test(
|
54
|
+
operation=operation,
|
55
|
+
test_func=test_func,
|
56
|
+
config=HypothesisTestConfig(
|
57
|
+
settings=settings,
|
58
|
+
seed=seed,
|
59
|
+
generation=generation_config,
|
60
|
+
as_strategy_kwargs=_as_strategy_kwargs,
|
61
|
+
given_kwargs=given_kwargs or {},
|
62
|
+
),
|
63
|
+
)
|
64
|
+
yield Ok((operation, test))
|
65
|
+
else:
|
66
|
+
yield result
|
67
|
+
|
68
|
+
|
69
|
+
@dataclass
|
70
|
+
class HypothesisTestConfig:
|
71
|
+
generation: GenerationConfig
|
72
|
+
settings: hypothesis.settings | None = None
|
73
|
+
seed: int | None = None
|
74
|
+
as_strategy_kwargs: dict[str, Any] = field(default_factory=dict)
|
75
|
+
given_args: tuple[GivenInput, ...] = ()
|
76
|
+
given_kwargs: dict[str, GivenInput] = field(default_factory=dict)
|
77
|
+
|
78
|
+
|
79
|
+
def create_test(
|
80
|
+
*,
|
81
|
+
operation: APIOperation,
|
82
|
+
test_func: Callable,
|
83
|
+
config: HypothesisTestConfig,
|
84
|
+
) -> Callable:
|
85
|
+
"""Create a Hypothesis test."""
|
86
|
+
hook_dispatcher = HookDispatcherMark.get(test_func)
|
87
|
+
auth_storage = AuthStorageMark.get(test_func)
|
88
|
+
|
89
|
+
strategy_kwargs = {
|
90
|
+
"hooks": hook_dispatcher,
|
91
|
+
"auth_storage": auth_storage,
|
92
|
+
"generation_config": config.generation,
|
93
|
+
**config.as_strategy_kwargs,
|
94
|
+
}
|
95
|
+
strategy = strategies.combine(
|
96
|
+
[operation.as_strategy(generation_mode=mode, **strategy_kwargs) for mode in config.generation.modes]
|
97
|
+
)
|
98
|
+
|
99
|
+
hypothesis_test = create_base_test(
|
100
|
+
test_function=test_func,
|
101
|
+
strategy=strategy,
|
102
|
+
args=config.given_args,
|
103
|
+
kwargs=config.given_kwargs,
|
104
|
+
)
|
105
|
+
|
106
|
+
if config.seed is not None:
|
107
|
+
hypothesis_test = hypothesis.seed(config.seed)(hypothesis_test)
|
108
|
+
|
109
|
+
default = hypothesis.settings.default
|
110
|
+
settings = getattr(hypothesis_test, SETTINGS_ATTRIBUTE_NAME, None)
|
111
|
+
assert settings is not None
|
112
|
+
|
113
|
+
if settings.deadline == default.deadline:
|
114
|
+
settings = hypothesis.settings(settings, deadline=DEFAULT_DEADLINE)
|
115
|
+
|
116
|
+
if config.settings is not None:
|
117
|
+
# Merge the user-provided settings with the current ones
|
118
|
+
settings = hypothesis.settings(
|
119
|
+
settings,
|
120
|
+
**{item: value for item, value in config.settings.__dict__.items() if value != getattr(default, item)},
|
121
|
+
)
|
122
|
+
|
123
|
+
if Phase.explain in settings.phases:
|
124
|
+
phases = tuple(phase for phase in settings.phases if phase != Phase.explain)
|
125
|
+
settings = hypothesis.settings(settings, phases=phases)
|
126
|
+
|
127
|
+
# Add examples if explicit phase is enabled
|
128
|
+
if Phase.explicit in settings.phases:
|
129
|
+
hypothesis_test = add_examples(hypothesis_test, operation, hook_dispatcher=hook_dispatcher, **strategy_kwargs)
|
130
|
+
|
131
|
+
if COVERAGE_PHASE.is_enabled:
|
132
|
+
# Ensure explicit phase is enabled if coverage is enabled
|
133
|
+
if Phase.explicit not in settings.phases:
|
134
|
+
phases = settings.phases + (Phase.explicit,)
|
135
|
+
settings = hypothesis.settings(settings, phases=phases)
|
136
|
+
hypothesis_test = add_coverage(hypothesis_test, operation, config.generation.modes)
|
137
|
+
|
138
|
+
setattr(hypothesis_test, SETTINGS_ATTRIBUTE_NAME, settings)
|
139
|
+
|
140
|
+
return hypothesis_test
|
141
|
+
|
142
|
+
|
143
|
+
SETTINGS_ATTRIBUTE_NAME = "_hypothesis_internal_use_settings"
|
144
|
+
|
145
|
+
|
146
|
+
def create_base_test(
|
147
|
+
*,
|
148
|
+
test_function: Callable,
|
149
|
+
strategy: st.SearchStrategy,
|
150
|
+
args: tuple[GivenInput, ...],
|
151
|
+
kwargs: dict[str, GivenInput],
|
152
|
+
) -> Callable:
|
153
|
+
"""Create the basic Hypothesis test with the given strategy."""
|
154
|
+
|
155
|
+
@wraps(test_function)
|
156
|
+
def test_wrapper(*args: Any, **kwargs: Any) -> Any:
|
157
|
+
__tracebackhide__ = True
|
158
|
+
return test_function(*args, **kwargs)
|
159
|
+
|
160
|
+
return hypothesis.given(*args, **{**kwargs, "case": strategy})(test_wrapper)
|
161
|
+
|
162
|
+
|
163
|
+
def add_examples(
|
164
|
+
test: Callable, operation: APIOperation, hook_dispatcher: HookDispatcher | None = None, **kwargs: Any
|
165
|
+
) -> Callable:
|
166
|
+
"""Add examples to the Hypothesis test, if they are specified in the schema."""
|
167
|
+
from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
|
168
|
+
|
169
|
+
try:
|
170
|
+
result: list[Case] = [
|
171
|
+
examples.generate_one(strategy) for strategy in operation.get_strategies_from_examples(**kwargs)
|
172
|
+
]
|
173
|
+
except (
|
174
|
+
InvalidSchema,
|
175
|
+
HypothesisRefResolutionError,
|
176
|
+
Unsatisfiable,
|
177
|
+
SerializationNotPossible,
|
178
|
+
SchemaError,
|
179
|
+
) as exc:
|
180
|
+
result = []
|
181
|
+
if isinstance(exc, Unsatisfiable):
|
182
|
+
UnsatisfiableExampleMark.set(test, exc)
|
183
|
+
if isinstance(exc, SerializationNotPossible):
|
184
|
+
NonSerializableMark.set(test, exc)
|
185
|
+
if isinstance(exc, SchemaError):
|
186
|
+
InvalidRegexMark.set(test, exc)
|
187
|
+
context = HookContext(operation) # context should be passed here instead
|
188
|
+
GLOBAL_HOOK_DISPATCHER.dispatch("before_add_examples", context, result)
|
189
|
+
operation.schema.hooks.dispatch("before_add_examples", context, result)
|
190
|
+
if hook_dispatcher:
|
191
|
+
hook_dispatcher.dispatch("before_add_examples", context, result)
|
192
|
+
original_test = test
|
193
|
+
for example in result:
|
194
|
+
if example.headers is not None:
|
195
|
+
invalid_headers = dict(find_invalid_headers(example.headers))
|
196
|
+
if invalid_headers:
|
197
|
+
InvalidHeadersExampleMark.set(original_test, invalid_headers)
|
198
|
+
continue
|
199
|
+
adjust_urlencoded_payload(example)
|
200
|
+
test = hypothesis.example(case=example)(test)
|
201
|
+
return test
|
202
|
+
|
203
|
+
|
204
|
+
def adjust_urlencoded_payload(case: Case) -> None:
|
205
|
+
if case.media_type is not None:
|
206
|
+
try:
|
207
|
+
media_type = media_types.parse(case.media_type)
|
208
|
+
if media_type == ("application", "x-www-form-urlencoded"):
|
209
|
+
case.body = prepare_urlencoded(case.body)
|
210
|
+
except ValueError:
|
211
|
+
pass
|
212
|
+
|
213
|
+
|
214
|
+
def add_coverage(test: Callable, operation: APIOperation, generation_modes: list[GenerationMode]) -> Callable:
|
215
|
+
for example in _iter_coverage_cases(operation, generation_modes):
|
216
|
+
adjust_urlencoded_payload(example)
|
217
|
+
test = hypothesis.example(case=example)(test)
|
218
|
+
return test
|
219
|
+
|
220
|
+
|
221
|
+
class Instant:
|
222
|
+
__slots__ = ("start",)
|
223
|
+
|
224
|
+
def __init__(self) -> None:
|
225
|
+
self.start = perf_counter()
|
226
|
+
|
227
|
+
@property
|
228
|
+
def elapsed(self) -> float:
|
229
|
+
return perf_counter() - self.start
|
230
|
+
|
231
|
+
|
232
|
+
def _iter_coverage_cases(
|
233
|
+
operation: APIOperation, generation_modes: list[GenerationMode]
|
234
|
+
) -> Generator[Case, None, None]:
|
235
|
+
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
|
236
|
+
from schemathesis.specs.openapi.examples import find_in_responses, find_matching_in_responses
|
237
|
+
|
238
|
+
def _stringify_value(val: Any, location: str) -> str | list[str]:
|
239
|
+
if isinstance(val, list):
|
240
|
+
if location == "query":
|
241
|
+
# Having a list here ensures there will be multiple query parameters wit the same name
|
242
|
+
return [json.dumps(item) for item in val]
|
243
|
+
# use comma-separated values style for arrays
|
244
|
+
return ",".join(json.dumps(sub) for sub in val)
|
245
|
+
return json.dumps(val)
|
246
|
+
|
247
|
+
generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
|
248
|
+
template: dict[str, Any] = {}
|
249
|
+
|
250
|
+
instant = Instant()
|
251
|
+
responses = find_in_responses(operation)
|
252
|
+
for parameter in operation.iter_parameters():
|
253
|
+
location = parameter.location
|
254
|
+
name = parameter.name
|
255
|
+
schema = parameter.as_json_schema(operation, update_quantifiers=False)
|
256
|
+
for value in find_matching_in_responses(responses, parameter.name):
|
257
|
+
schema.setdefault("examples", []).append(value)
|
258
|
+
gen = coverage.cover_schema_iter(
|
259
|
+
coverage.CoverageContext(location=location, generation_modes=generation_modes), schema
|
260
|
+
)
|
261
|
+
value = next(gen, NOT_SET)
|
262
|
+
if isinstance(value, NotSet):
|
263
|
+
continue
|
264
|
+
container = template.setdefault(LOCATION_TO_CONTAINER[location], {})
|
265
|
+
if location in ("header", "cookie", "path", "query") and not isinstance(value.value, str):
|
266
|
+
container[name] = _stringify_value(value.value, location)
|
267
|
+
else:
|
268
|
+
container[name] = value.value
|
269
|
+
generators[(location, name)] = gen
|
270
|
+
template_time = instant.elapsed
|
271
|
+
if operation.body:
|
272
|
+
for body in operation.body:
|
273
|
+
instant = Instant()
|
274
|
+
schema = body.as_json_schema(operation, update_quantifiers=False)
|
275
|
+
# Definition could be a list for Open API 2.0
|
276
|
+
definition = body.definition if isinstance(body.definition, dict) else {}
|
277
|
+
examples = [example["value"] for example in definition.get("examples", {}).values() if "value" in example]
|
278
|
+
if examples:
|
279
|
+
schema.setdefault("examples", []).extend(examples)
|
280
|
+
gen = coverage.cover_schema_iter(
|
281
|
+
coverage.CoverageContext(location="body", generation_modes=generation_modes), schema
|
282
|
+
)
|
283
|
+
value = next(gen, NOT_SET)
|
284
|
+
if isinstance(value, NotSet):
|
285
|
+
continue
|
286
|
+
elapsed = instant.elapsed
|
287
|
+
if "body" not in template:
|
288
|
+
template_time += elapsed
|
289
|
+
template["body"] = value.value
|
290
|
+
template["media_type"] = body.media_type
|
291
|
+
yield operation.Case(
|
292
|
+
**{**template, "body": value.value, "media_type": body.media_type},
|
293
|
+
meta=CaseMetadata(
|
294
|
+
generation=GenerationInfo(
|
295
|
+
time=elapsed,
|
296
|
+
mode=value.generation_mode,
|
297
|
+
),
|
298
|
+
components={},
|
299
|
+
phase=PhaseInfo.coverage(
|
300
|
+
description=value.description,
|
301
|
+
location=value.location,
|
302
|
+
parameter=body.media_type,
|
303
|
+
parameter_location="body",
|
304
|
+
),
|
305
|
+
),
|
306
|
+
)
|
307
|
+
iterator = iter(gen)
|
308
|
+
while True:
|
309
|
+
instant = Instant()
|
310
|
+
try:
|
311
|
+
next_value = next(iterator)
|
312
|
+
yield operation.Case(
|
313
|
+
**{**template, "body": next_value.value, "media_type": body.media_type},
|
314
|
+
meta=CaseMetadata(
|
315
|
+
generation=GenerationInfo(
|
316
|
+
time=instant.elapsed,
|
317
|
+
mode=value.generation_mode,
|
318
|
+
),
|
319
|
+
components={},
|
320
|
+
phase=PhaseInfo.coverage(
|
321
|
+
description=next_value.description,
|
322
|
+
location=next_value.location,
|
323
|
+
parameter=body.media_type,
|
324
|
+
parameter_location="body",
|
325
|
+
),
|
326
|
+
),
|
327
|
+
)
|
328
|
+
except StopIteration:
|
329
|
+
break
|
330
|
+
elif GenerationMode.POSITIVE in generation_modes:
|
331
|
+
yield operation.Case(
|
332
|
+
**template,
|
333
|
+
meta=CaseMetadata(
|
334
|
+
generation=GenerationInfo(
|
335
|
+
time=template_time,
|
336
|
+
mode=GenerationMode.POSITIVE,
|
337
|
+
),
|
338
|
+
components={},
|
339
|
+
phase=PhaseInfo.coverage(description="Default positive test case"),
|
340
|
+
),
|
341
|
+
)
|
342
|
+
|
343
|
+
for (location, name), gen in generators.items():
|
344
|
+
container_name = LOCATION_TO_CONTAINER[location]
|
345
|
+
container = template[container_name]
|
346
|
+
iterator = iter(gen)
|
347
|
+
while True:
|
348
|
+
instant = Instant()
|
349
|
+
try:
|
350
|
+
value = next(iterator)
|
351
|
+
if location in ("header", "cookie", "path", "query") and not isinstance(value.value, str):
|
352
|
+
generated = _stringify_value(value.value, location)
|
353
|
+
else:
|
354
|
+
generated = value.value
|
355
|
+
except StopIteration:
|
356
|
+
break
|
357
|
+
yield operation.Case(
|
358
|
+
**{**template, container_name: {**container, name: generated}},
|
359
|
+
meta=CaseMetadata(
|
360
|
+
generation=GenerationInfo(time=instant.elapsed, mode=value.generation_mode),
|
361
|
+
components={},
|
362
|
+
phase=PhaseInfo.coverage(
|
363
|
+
description=value.description,
|
364
|
+
location=value.location,
|
365
|
+
parameter=name,
|
366
|
+
parameter_location=location,
|
367
|
+
),
|
368
|
+
),
|
369
|
+
)
|
370
|
+
if GenerationMode.NEGATIVE in generation_modes:
|
371
|
+
# Generate HTTP methods that are not specified in the spec
|
372
|
+
methods = {"get", "put", "post", "delete", "options", "patch", "trace"} - set(operation.schema[operation.path])
|
373
|
+
for method in sorted(methods):
|
374
|
+
instant = Instant()
|
375
|
+
yield operation.Case(
|
376
|
+
**template,
|
377
|
+
method=method.upper(),
|
378
|
+
meta=CaseMetadata(
|
379
|
+
generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
|
380
|
+
components={},
|
381
|
+
phase=PhaseInfo.coverage(description=f"Unspecified HTTP method: {method.upper()}"),
|
382
|
+
),
|
383
|
+
)
|
384
|
+
# Generate duplicate query parameters
|
385
|
+
if operation.query:
|
386
|
+
container = template["query"]
|
387
|
+
for parameter in operation.query:
|
388
|
+
instant = Instant()
|
389
|
+
value = container[parameter.name]
|
390
|
+
yield operation.Case(
|
391
|
+
**{**template, "query": {**container, parameter.name: [value, value]}},
|
392
|
+
meta=CaseMetadata(
|
393
|
+
generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
|
394
|
+
components={},
|
395
|
+
phase=PhaseInfo.coverage(
|
396
|
+
description=f"Duplicate `{parameter.name}` query parameter",
|
397
|
+
parameter=parameter.name,
|
398
|
+
parameter_location="query",
|
399
|
+
),
|
400
|
+
),
|
401
|
+
)
|
402
|
+
# Generate missing required parameters
|
403
|
+
for parameter in operation.iter_parameters():
|
404
|
+
if parameter.is_required and parameter.location != "path":
|
405
|
+
instant = Instant()
|
406
|
+
name = parameter.name
|
407
|
+
location = parameter.location
|
408
|
+
container_name = LOCATION_TO_CONTAINER[location]
|
409
|
+
container = template[container_name]
|
410
|
+
yield operation.Case(
|
411
|
+
**{**template, container_name: {k: v for k, v in container.items() if k != name}},
|
412
|
+
meta=CaseMetadata(
|
413
|
+
generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
|
414
|
+
components={},
|
415
|
+
phase=PhaseInfo.coverage(
|
416
|
+
description=f"Missing `{name}` at {location}",
|
417
|
+
parameter=name,
|
418
|
+
parameter_location=location,
|
419
|
+
),
|
420
|
+
),
|
421
|
+
)
|
422
|
+
# Generate combinations for each location
|
423
|
+
for location, parameter_set in [
|
424
|
+
("query", operation.query),
|
425
|
+
("header", operation.headers),
|
426
|
+
("cookie", operation.cookies),
|
427
|
+
]:
|
428
|
+
if not parameter_set:
|
429
|
+
continue
|
430
|
+
|
431
|
+
container_name = LOCATION_TO_CONTAINER[location]
|
432
|
+
base_container = template.get(container_name, {})
|
433
|
+
|
434
|
+
# Get required and optional parameters
|
435
|
+
required = {p.name for p in parameter_set if p.is_required}
|
436
|
+
all_params = {p.name for p in parameter_set}
|
437
|
+
optional = sorted(all_params - required)
|
438
|
+
|
439
|
+
# Helper function to create and yield a case
|
440
|
+
def make_case(
|
441
|
+
container_values: dict,
|
442
|
+
description: str,
|
443
|
+
_location: str,
|
444
|
+
_container_name: str,
|
445
|
+
_parameter: str | None,
|
446
|
+
_generation_mode: GenerationMode,
|
447
|
+
_instant: Instant,
|
448
|
+
) -> Case:
|
449
|
+
if _location in ("header", "cookie", "path", "query"):
|
450
|
+
container = {
|
451
|
+
name: _stringify_value(val, _location) if not isinstance(val, str) else val
|
452
|
+
for name, val in container_values.items()
|
453
|
+
}
|
454
|
+
else:
|
455
|
+
container = container_values
|
456
|
+
|
457
|
+
return operation.Case(
|
458
|
+
**{**template, _container_name: container},
|
459
|
+
meta=CaseMetadata(
|
460
|
+
generation=GenerationInfo(
|
461
|
+
time=_instant.elapsed,
|
462
|
+
mode=_generation_mode,
|
463
|
+
),
|
464
|
+
components={},
|
465
|
+
phase=PhaseInfo.coverage(
|
466
|
+
description=description,
|
467
|
+
parameter=_parameter,
|
468
|
+
parameter_location=_location,
|
469
|
+
),
|
470
|
+
),
|
471
|
+
)
|
472
|
+
|
473
|
+
def _combination_schema(
|
474
|
+
combination: dict[str, Any], _required: set[str], _parameter_set: ParameterSet
|
475
|
+
) -> dict[str, Any]:
|
476
|
+
return {
|
477
|
+
"properties": {
|
478
|
+
parameter.name: parameter.as_json_schema(operation)
|
479
|
+
for parameter in _parameter_set
|
480
|
+
if parameter.name in combination
|
481
|
+
},
|
482
|
+
"required": list(_required),
|
483
|
+
"additionalProperties": False,
|
484
|
+
}
|
485
|
+
|
486
|
+
def _yield_negative(
|
487
|
+
subschema: dict[str, Any], _location: str, _container_name: str
|
488
|
+
) -> Generator[Case, None, None]:
|
489
|
+
iterator = iter(
|
490
|
+
coverage.cover_schema_iter(
|
491
|
+
coverage.CoverageContext(location=_location, generation_modes=[GenerationMode.NEGATIVE]),
|
492
|
+
subschema,
|
493
|
+
)
|
494
|
+
)
|
495
|
+
while True:
|
496
|
+
instant = Instant()
|
497
|
+
try:
|
498
|
+
more = next(iterator)
|
499
|
+
yield make_case(
|
500
|
+
more.value,
|
501
|
+
more.description,
|
502
|
+
_location,
|
503
|
+
_container_name,
|
504
|
+
more.parameter,
|
505
|
+
GenerationMode.NEGATIVE,
|
506
|
+
instant,
|
507
|
+
)
|
508
|
+
except StopIteration:
|
509
|
+
break
|
510
|
+
|
511
|
+
# 1. Generate only required properties
|
512
|
+
if required and all_params != required:
|
513
|
+
only_required = {k: v for k, v in base_container.items() if k in required}
|
514
|
+
if GenerationMode.POSITIVE in generation_modes:
|
515
|
+
yield make_case(
|
516
|
+
only_required,
|
517
|
+
"Only required properties",
|
518
|
+
location,
|
519
|
+
container_name,
|
520
|
+
None,
|
521
|
+
GenerationMode.POSITIVE,
|
522
|
+
Instant(),
|
523
|
+
)
|
524
|
+
if GenerationMode.NEGATIVE in generation_modes:
|
525
|
+
subschema = _combination_schema(only_required, required, parameter_set)
|
526
|
+
for case in _yield_negative(subschema, location, container_name):
|
527
|
+
assert case.meta is not None
|
528
|
+
assert isinstance(case.meta.phase.data, CoveragePhaseData)
|
529
|
+
# Already generated in one of the blocks above
|
530
|
+
if location != "path" and not case.meta.phase.data.description.startswith(
|
531
|
+
"Missing required property"
|
532
|
+
):
|
533
|
+
yield case
|
534
|
+
|
535
|
+
# 2. Generate combinations with required properties and one optional property
|
536
|
+
for opt_param in optional:
|
537
|
+
combo = {k: v for k, v in base_container.items() if k in required or k == opt_param}
|
538
|
+
if combo != base_container and GenerationMode.POSITIVE in generation_modes:
|
539
|
+
yield make_case(
|
540
|
+
combo,
|
541
|
+
f"All required properties and optional '{opt_param}'",
|
542
|
+
location,
|
543
|
+
container_name,
|
544
|
+
None,
|
545
|
+
GenerationMode.POSITIVE,
|
546
|
+
Instant(),
|
547
|
+
)
|
548
|
+
if GenerationMode.NEGATIVE in generation_modes:
|
549
|
+
subschema = _combination_schema(combo, required, parameter_set)
|
550
|
+
for case in _yield_negative(subschema, location, container_name):
|
551
|
+
assert case.meta is not None
|
552
|
+
assert isinstance(case.meta.phase.data, CoveragePhaseData)
|
553
|
+
# Already generated in one of the blocks above
|
554
|
+
if location != "path" and not case.meta.phase.data.description.startswith(
|
555
|
+
"Missing required property"
|
556
|
+
):
|
557
|
+
yield case
|
558
|
+
|
559
|
+
# 3. Generate one combination for each size from 2 to N-1 of optional parameters
|
560
|
+
if len(optional) > 1 and GenerationMode.POSITIVE in generation_modes:
|
561
|
+
for size in range(2, len(optional)):
|
562
|
+
for combination in combinations(optional, size):
|
563
|
+
combo = {k: v for k, v in base_container.items() if k in required or k in combination}
|
564
|
+
if combo != base_container:
|
565
|
+
yield make_case(
|
566
|
+
combo,
|
567
|
+
f"All required and {size} optional properties",
|
568
|
+
location,
|
569
|
+
container_name,
|
570
|
+
None,
|
571
|
+
GenerationMode.POSITIVE,
|
572
|
+
Instant(),
|
573
|
+
)
|
574
|
+
|
575
|
+
|
576
|
+
def find_invalid_headers(headers: Mapping) -> Generator[tuple[str, str], None, None]:
|
577
|
+
for name, value in headers.items():
|
578
|
+
if not is_latin_1_encodable(value) or has_invalid_characters(name, value):
|
579
|
+
yield name, value
|
580
|
+
|
581
|
+
|
582
|
+
UnsatisfiableExampleMark = Mark[Unsatisfiable](attr_name="unsatisfiable_example")
|
583
|
+
NonSerializableMark = Mark[SerializationNotPossible](attr_name="non_serializable")
|
584
|
+
InvalidRegexMark = Mark[SchemaError](attr_name="invalid_regex")
|
585
|
+
InvalidHeadersExampleMark = Mark[dict[str, str]](attr_name="invalid_example_header")
|
@@ -0,0 +1,50 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import os
|
4
|
+
from functools import lru_cache
|
5
|
+
from typing import TYPE_CHECKING, TypeVar
|
6
|
+
|
7
|
+
if TYPE_CHECKING:
|
8
|
+
from hypothesis import settings
|
9
|
+
from hypothesis import strategies as st
|
10
|
+
|
11
|
+
SCHEMATHESIS_BENCHMARK_SEED = os.environ.get("SCHEMATHESIS_BENCHMARK_SEED")
|
12
|
+
|
13
|
+
|
14
|
+
@lru_cache
|
15
|
+
def default_settings() -> settings:
|
16
|
+
from hypothesis import HealthCheck, Phase, Verbosity, settings
|
17
|
+
|
18
|
+
return settings(
|
19
|
+
database=None,
|
20
|
+
max_examples=1,
|
21
|
+
deadline=None,
|
22
|
+
verbosity=Verbosity.quiet,
|
23
|
+
phases=(Phase.generate,),
|
24
|
+
suppress_health_check=list(HealthCheck),
|
25
|
+
)
|
26
|
+
|
27
|
+
|
28
|
+
T = TypeVar("T")
|
29
|
+
|
30
|
+
|
31
|
+
def generate_one(strategy: st.SearchStrategy[T]) -> T: # type: ignore[type-var]
|
32
|
+
examples: list[T] = []
|
33
|
+
add_single_example(strategy, examples)
|
34
|
+
return examples[0]
|
35
|
+
|
36
|
+
|
37
|
+
def add_single_example(strategy: st.SearchStrategy[T], examples: list[T]) -> None:
|
38
|
+
from hypothesis import given, seed
|
39
|
+
|
40
|
+
@given(strategy) # type: ignore
|
41
|
+
@default_settings() # type: ignore
|
42
|
+
def example_generating_inner_function(ex: T) -> None:
|
43
|
+
examples.append(ex)
|
44
|
+
|
45
|
+
example_generating_inner_function._hypothesis_internal_database_key = b"" # type: ignore
|
46
|
+
|
47
|
+
if SCHEMATHESIS_BENCHMARK_SEED is not None:
|
48
|
+
example_generating_inner_function = seed(SCHEMATHESIS_BENCHMARK_SEED)(example_generating_inner_function)
|
49
|
+
|
50
|
+
example_generating_inner_function()
|