schemathesis 3.34.3__py3-none-any.whl → 3.35.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/_hypothesis.py +55 -26
- schemathesis/cli/__init__.py +25 -4
- schemathesis/cli/context.py +4 -0
- schemathesis/cli/handlers.py +4 -1
- schemathesis/cli/output/default.py +4 -0
- schemathesis/contrib/openapi/fill_missing_examples.py +1 -1
- schemathesis/experimental/__init__.py +7 -0
- schemathesis/generation/__init__.py +4 -37
- schemathesis/generation/_hypothesis.py +51 -0
- schemathesis/generation/_methods.py +40 -0
- schemathesis/generation/coverage.py +433 -0
- schemathesis/schemas.py +2 -1
- schemathesis/service/extensions.py +1 -1
- schemathesis/specs/openapi/examples.py +1 -1
- schemathesis/specs/openapi/schemas.py +2 -1
- schemathesis/specs/openapi/stateful/__init__.py +1 -2
- schemathesis/utils.py +0 -10
- {schemathesis-3.34.3.dist-info → schemathesis-3.35.0.dist-info}/METADATA +1 -1
- {schemathesis-3.34.3.dist-info → schemathesis-3.35.0.dist-info}/RECORD +22 -19
- {schemathesis-3.34.3.dist-info → schemathesis-3.35.0.dist-info}/WHEEL +0 -0
- {schemathesis-3.34.3.dist-info → schemathesis-3.35.0.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.34.3.dist-info → schemathesis-3.35.0.dist-info}/licenses/LICENSE +0 -0
schemathesis/_hypothesis.py
CHANGED
|
@@ -8,21 +8,21 @@ from typing import Any, Callable, Generator, Mapping, Optional, Tuple
|
|
|
8
8
|
|
|
9
9
|
import hypothesis
|
|
10
10
|
from hypothesis import Phase
|
|
11
|
-
from hypothesis import strategies as st
|
|
12
11
|
from hypothesis.errors import HypothesisWarning, Unsatisfiable
|
|
13
12
|
from hypothesis.internal.entropy import deterministic_PRNG
|
|
14
13
|
from hypothesis.internal.reflection import proxies
|
|
15
14
|
from jsonschema.exceptions import SchemaError
|
|
16
15
|
|
|
17
16
|
from .auths import get_auth_storage_from_test
|
|
18
|
-
from .constants import DEFAULT_DEADLINE
|
|
17
|
+
from .constants import DEFAULT_DEADLINE, NOT_SET
|
|
19
18
|
from .exceptions import OperationSchemaError, SerializationNotPossible
|
|
20
|
-
from .
|
|
19
|
+
from .experimental import COVERAGE_PHASE
|
|
20
|
+
from .generation import DataGenerationMethod, GenerationConfig, combine_strategies, coverage, get_single_example
|
|
21
21
|
from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher
|
|
22
22
|
from .models import APIOperation, Case
|
|
23
23
|
from .transports.content_types import parse_content_type
|
|
24
24
|
from .transports.headers import has_invalid_characters, is_latin_1_encodable
|
|
25
|
-
from .utils import GivenInput
|
|
25
|
+
from .utils import GivenInput
|
|
26
26
|
|
|
27
27
|
# Forcefully initializes Hypothesis' global PRNG to avoid races that initilize it
|
|
28
28
|
# if e.g. Schemathesis CLI is used with multiple workers
|
|
@@ -101,6 +101,8 @@ def create_test(
|
|
|
101
101
|
wrapped_test = add_examples(
|
|
102
102
|
wrapped_test, operation, hook_dispatcher=hook_dispatcher, as_strategy_kwargs=as_strategy_kwargs
|
|
103
103
|
)
|
|
104
|
+
if COVERAGE_PHASE.is_enabled:
|
|
105
|
+
wrapped_test = add_coverage(wrapped_test, operation, data_generation_methods)
|
|
104
106
|
return wrapped_test
|
|
105
107
|
|
|
106
108
|
|
|
@@ -199,6 +201,55 @@ def add_examples(
|
|
|
199
201
|
return test
|
|
200
202
|
|
|
201
203
|
|
|
204
|
+
def add_coverage(
|
|
205
|
+
test: Callable, operation: APIOperation, data_generation_methods: list[DataGenerationMethod]
|
|
206
|
+
) -> Callable:
|
|
207
|
+
for example in _iter_coverage_cases(operation, data_generation_methods):
|
|
208
|
+
test = hypothesis.example(case=example)(test)
|
|
209
|
+
return test
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _iter_coverage_cases(
|
|
213
|
+
operation: APIOperation, data_generation_methods: list[DataGenerationMethod]
|
|
214
|
+
) -> Generator[Case, None, None]:
|
|
215
|
+
from .specs.openapi.constants import LOCATION_TO_CONTAINER
|
|
216
|
+
|
|
217
|
+
ctx = coverage.CoverageContext(data_generation_methods=data_generation_methods)
|
|
218
|
+
generators: dict[tuple[str, str], Generator] = {}
|
|
219
|
+
template: dict[str, Any] = {}
|
|
220
|
+
for parameter in operation.iter_parameters():
|
|
221
|
+
schema = parameter.as_json_schema(operation)
|
|
222
|
+
gen = coverage.cover_schema_iter(ctx, schema)
|
|
223
|
+
value = next(gen, NOT_SET)
|
|
224
|
+
if value is NOT_SET:
|
|
225
|
+
continue
|
|
226
|
+
location = parameter.location
|
|
227
|
+
name = parameter.name
|
|
228
|
+
container = template.setdefault(LOCATION_TO_CONTAINER[location], {})
|
|
229
|
+
container[name] = value
|
|
230
|
+
generators[(location, name)] = gen
|
|
231
|
+
if operation.body:
|
|
232
|
+
for body in operation.body:
|
|
233
|
+
schema = body.as_json_schema(operation)
|
|
234
|
+
gen = coverage.cover_schema_iter(ctx, schema)
|
|
235
|
+
value = next(gen, NOT_SET)
|
|
236
|
+
if value is NOT_SET:
|
|
237
|
+
continue
|
|
238
|
+
if "body" not in template:
|
|
239
|
+
template["body"] = value
|
|
240
|
+
template["media_type"] = body.media_type
|
|
241
|
+
yield operation.make_case(**{**template, "body": value, "media_type": body.media_type})
|
|
242
|
+
for next_value in gen:
|
|
243
|
+
yield operation.make_case(**{**template, "body": next_value, "media_type": body.media_type})
|
|
244
|
+
else:
|
|
245
|
+
yield operation.make_case(**template)
|
|
246
|
+
for (location, name), gen in generators.items():
|
|
247
|
+
container_name = LOCATION_TO_CONTAINER[location]
|
|
248
|
+
container = template[container_name]
|
|
249
|
+
for value in gen:
|
|
250
|
+
yield operation.make_case(**{**template, container_name: {**container, name: value}})
|
|
251
|
+
|
|
252
|
+
|
|
202
253
|
def find_invalid_headers(headers: Mapping) -> Generator[Tuple[str, str], None, None]:
|
|
203
254
|
for name, value in headers.items():
|
|
204
255
|
if not is_latin_1_encodable(value) or has_invalid_characters(name, value):
|
|
@@ -248,25 +299,3 @@ def get_invalid_example_headers_mark(test: Callable) -> Optional[dict[str, str]]
|
|
|
248
299
|
|
|
249
300
|
def add_invalid_example_header_mark(test: Callable, headers: dict[str, str]) -> None:
|
|
250
301
|
test._schemathesis_invalid_example_headers = headers # type: ignore
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
def get_single_example(strategy: st.SearchStrategy[Case]) -> Case:
|
|
254
|
-
examples: list[Case] = []
|
|
255
|
-
add_single_example(strategy, examples)
|
|
256
|
-
return examples[0]
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
def add_single_example(strategy: st.SearchStrategy[Case], examples: list[Case]) -> None:
|
|
260
|
-
@hypothesis.given(strategy) # type: ignore
|
|
261
|
-
@hypothesis.settings( # type: ignore
|
|
262
|
-
database=None,
|
|
263
|
-
max_examples=1,
|
|
264
|
-
deadline=None,
|
|
265
|
-
verbosity=hypothesis.Verbosity.quiet,
|
|
266
|
-
phases=(hypothesis.Phase.generate,),
|
|
267
|
-
suppress_health_check=list(hypothesis.HealthCheck),
|
|
268
|
-
)
|
|
269
|
-
def example_generating_inner_function(ex: Case) -> None:
|
|
270
|
-
examples.append(ex)
|
|
271
|
-
|
|
272
|
-
example_generating_inner_function()
|
schemathesis/cli/__init__.py
CHANGED
|
@@ -11,7 +11,7 @@ from collections import defaultdict
|
|
|
11
11
|
from dataclasses import dataclass
|
|
12
12
|
from enum import Enum
|
|
13
13
|
from queue import Queue
|
|
14
|
-
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, Literal, NoReturn, Sequence, cast
|
|
14
|
+
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, Literal, NoReturn, Sequence, Type, cast
|
|
15
15
|
from urllib.parse import urlparse
|
|
16
16
|
|
|
17
17
|
import click
|
|
@@ -42,7 +42,7 @@ from ..internal.datetime import current_datetime
|
|
|
42
42
|
from ..internal.output import OutputConfig
|
|
43
43
|
from ..internal.validation import file_exists
|
|
44
44
|
from ..loaders import load_app, load_yaml
|
|
45
|
-
from ..models import
|
|
45
|
+
from ..models import Case, CheckFunction
|
|
46
46
|
from ..runner import events, prepare_hypothesis_settings, probes
|
|
47
47
|
from ..specs.graphql import loaders as gql_loaders
|
|
48
48
|
from ..specs.openapi import loaders as oas_loaders
|
|
@@ -50,11 +50,12 @@ from ..stateful import Stateful
|
|
|
50
50
|
from ..targets import Target
|
|
51
51
|
from ..transports import RequestConfig
|
|
52
52
|
from ..transports.auth import get_requests_auth
|
|
53
|
-
from ..types import
|
|
53
|
+
from ..types import PathLike, RequestCert
|
|
54
54
|
from . import callbacks, cassettes, output
|
|
55
55
|
from .constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS, HealthCheck, Phase, Verbosity
|
|
56
56
|
from .context import ExecutionContext, FileReportContext, ServiceReportContext
|
|
57
57
|
from .debug import DebugOutputHandler
|
|
58
|
+
from .handlers import EventHandler
|
|
58
59
|
from .junitxml import JunitXMLHandler
|
|
59
60
|
from .options import CsvChoice, CsvEnumChoice, CustomHelpMessageChoice, NotSet, OptionalInt
|
|
60
61
|
from .sanitization import SanitizationHandler
|
|
@@ -66,13 +67,13 @@ if TYPE_CHECKING:
|
|
|
66
67
|
from ..schemas import BaseSchema
|
|
67
68
|
from ..service.client import ServiceClient
|
|
68
69
|
from ..specs.graphql.schemas import GraphQLSchema
|
|
69
|
-
from .handlers import EventHandler
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
def _get_callable_names(items: tuple[Callable, ...]) -> tuple[str, ...]:
|
|
73
73
|
return tuple(item.__name__ for item in items)
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
CUSTOM_HANDLERS: list[Type[EventHandler]] = []
|
|
76
77
|
CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
|
|
77
78
|
|
|
78
79
|
DEFAULT_CHECKS_NAMES = _get_callable_names(checks_module.DEFAULT_CHECKS)
|
|
@@ -759,6 +760,7 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
|
|
|
759
760
|
experimental.SCHEMA_ANALYSIS.name,
|
|
760
761
|
experimental.STATEFUL_TEST_RUNNER.name,
|
|
761
762
|
experimental.STATEFUL_ONLY.name,
|
|
763
|
+
experimental.COVERAGE_PHASE.name,
|
|
762
764
|
]
|
|
763
765
|
),
|
|
764
766
|
callback=callbacks.convert_experimental,
|
|
@@ -927,6 +929,7 @@ def run(
|
|
|
927
929
|
schemathesis_io_telemetry: bool = True,
|
|
928
930
|
hosts_file: PathLike = service.DEFAULT_HOSTS_PATH,
|
|
929
931
|
force_color: bool = False,
|
|
932
|
+
**__kwargs,
|
|
930
933
|
) -> None:
|
|
931
934
|
"""Run tests against an API using a specified SCHEMA.
|
|
932
935
|
|
|
@@ -1212,6 +1215,7 @@ def run(
|
|
|
1212
1215
|
)
|
|
1213
1216
|
execute(
|
|
1214
1217
|
event_stream,
|
|
1218
|
+
ctx=ctx,
|
|
1215
1219
|
hypothesis_settings=hypothesis_settings,
|
|
1216
1220
|
workers_num=workers_num,
|
|
1217
1221
|
rate_limit=rate_limit,
|
|
@@ -1595,6 +1599,7 @@ class OutputStyle(Enum):
|
|
|
1595
1599
|
def execute(
|
|
1596
1600
|
event_stream: Generator[events.ExecutionEvent, None, None],
|
|
1597
1601
|
*,
|
|
1602
|
+
ctx: click.Context,
|
|
1598
1603
|
hypothesis_settings: hypothesis.settings,
|
|
1599
1604
|
workers_num: int,
|
|
1600
1605
|
rate_limit: str | None,
|
|
@@ -1669,6 +1674,8 @@ def execute(
|
|
|
1669
1674
|
cassette_path, format=cassette_format, preserve_exact_body_bytes=cassette_preserve_exact_body_bytes
|
|
1670
1675
|
)
|
|
1671
1676
|
)
|
|
1677
|
+
for custom_handler in CUSTOM_HANDLERS:
|
|
1678
|
+
handlers.append(custom_handler(*ctx.args, **ctx.params))
|
|
1672
1679
|
handlers.append(get_output_handler(workers_num))
|
|
1673
1680
|
if sanitize_output:
|
|
1674
1681
|
handlers.insert(0, SanitizationHandler())
|
|
@@ -2000,6 +2007,20 @@ def decide_color_output(ctx: click.Context, no_color: bool, force_color: bool) -
|
|
|
2000
2007
|
ctx.color = False
|
|
2001
2008
|
|
|
2002
2009
|
|
|
2010
|
+
def add_option(*args: Any, cls: Type = click.Option, **kwargs: Any) -> None:
|
|
2011
|
+
"""Add a new CLI option to `st run`."""
|
|
2012
|
+
run.params.append(cls(args, **kwargs))
|
|
2013
|
+
|
|
2014
|
+
|
|
2015
|
+
def handler() -> Callable[[Type], None]:
|
|
2016
|
+
"""Register a new CLI event handler."""
|
|
2017
|
+
|
|
2018
|
+
def _wrapper(cls: Type) -> None:
|
|
2019
|
+
CUSTOM_HANDLERS.append(cls)
|
|
2020
|
+
|
|
2021
|
+
return _wrapper
|
|
2022
|
+
|
|
2023
|
+
|
|
2003
2024
|
@HookDispatcher.register_spec([HookScope.GLOBAL])
|
|
2004
2025
|
def after_init_cli_run_handlers(
|
|
2005
2026
|
context: HookContext, handlers: list[EventHandler], execution_context: ExecutionContext
|
schemathesis/cli/context.py
CHANGED
|
@@ -60,7 +60,11 @@ class ExecutionContext:
|
|
|
60
60
|
analysis: Result[AnalysisResult, Exception] | None = None
|
|
61
61
|
output_config: OutputConfig = field(default_factory=OutputConfig)
|
|
62
62
|
state_machine_sink: StateMachineSink | None = None
|
|
63
|
+
summary_lines: list[str] = field(default_factory=list)
|
|
63
64
|
|
|
64
65
|
@deprecated_property(removed_in="4.0", replacement="show_trace")
|
|
65
66
|
def show_errors_tracebacks(self) -> bool:
|
|
66
67
|
return self.show_trace
|
|
68
|
+
|
|
69
|
+
def add_summary_line(self, line: str) -> None:
|
|
70
|
+
self.summary_lines.append(line)
|
schemathesis/cli/handlers.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import TYPE_CHECKING
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
4
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
6
|
from ..runner import events
|
|
@@ -8,6 +8,9 @@ if TYPE_CHECKING:
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class EventHandler:
|
|
11
|
+
def __init__(self, *args: Any, **params: Any) -> None:
|
|
12
|
+
pass
|
|
13
|
+
|
|
11
14
|
def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
|
|
12
15
|
raise NotImplementedError
|
|
13
16
|
|
|
@@ -850,6 +850,10 @@ def handle_finished(context: ExecutionContext, event: events.Finished) -> None:
|
|
|
850
850
|
display_application_logs(context, event)
|
|
851
851
|
display_analysis(context)
|
|
852
852
|
display_statistic(context, event)
|
|
853
|
+
if context.summary_lines:
|
|
854
|
+
click.echo()
|
|
855
|
+
for line in context.summary_lines:
|
|
856
|
+
click.echo(line)
|
|
853
857
|
click.echo()
|
|
854
858
|
display_summary(event)
|
|
855
859
|
|
|
@@ -18,7 +18,7 @@ def uninstall() -> None:
|
|
|
18
18
|
|
|
19
19
|
def before_add_examples(context: HookContext, examples: list[Case]) -> None:
|
|
20
20
|
if not examples and context.operation is not None:
|
|
21
|
-
from ...
|
|
21
|
+
from ...generation import add_single_example
|
|
22
22
|
|
|
23
23
|
strategy = context.operation.as_strategy()
|
|
24
24
|
add_single_example(strategy, examples)
|
|
@@ -93,3 +93,10 @@ STATEFUL_ONLY = GLOBAL_EXPERIMENTS.create_experiment(
|
|
|
93
93
|
description="Run only stateful tests",
|
|
94
94
|
discussion_url="https://github.com/schemathesis/schemathesis/discussions/2262",
|
|
95
95
|
)
|
|
96
|
+
COVERAGE_PHASE = GLOBAL_EXPERIMENTS.create_experiment(
|
|
97
|
+
name="coverage-phase",
|
|
98
|
+
verbose_name="Coverage phase",
|
|
99
|
+
env_var="COVERAGE_PHASE",
|
|
100
|
+
description="Generate covering test cases",
|
|
101
|
+
discussion_url="https://github.com/schemathesis/schemathesis/discussions/2418",
|
|
102
|
+
)
|
|
@@ -2,48 +2,15 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import random
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
|
-
from
|
|
6
|
-
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from ._hypothesis import add_single_example, combine_strategies, get_single_example # noqa: E402
|
|
8
|
+
from ._methods import DataGenerationMethod, DataGenerationMethodInput # noqa: E402
|
|
7
9
|
|
|
8
10
|
if TYPE_CHECKING:
|
|
9
11
|
from hypothesis.strategies import SearchStrategy
|
|
10
12
|
|
|
11
13
|
|
|
12
|
-
class DataGenerationMethod(str, Enum):
|
|
13
|
-
"""Defines what data Schemathesis generates for tests."""
|
|
14
|
-
|
|
15
|
-
# Generate data, that fits the API schema
|
|
16
|
-
positive = "positive"
|
|
17
|
-
# Doesn't fit the API schema
|
|
18
|
-
negative = "negative"
|
|
19
|
-
|
|
20
|
-
@classmethod
|
|
21
|
-
def default(cls) -> DataGenerationMethod:
|
|
22
|
-
return cls.positive
|
|
23
|
-
|
|
24
|
-
@classmethod
|
|
25
|
-
def all(cls) -> list[DataGenerationMethod]:
|
|
26
|
-
return list(DataGenerationMethod)
|
|
27
|
-
|
|
28
|
-
def as_short_name(self) -> str:
|
|
29
|
-
return {
|
|
30
|
-
DataGenerationMethod.positive: "P",
|
|
31
|
-
DataGenerationMethod.negative: "N",
|
|
32
|
-
}[self]
|
|
33
|
-
|
|
34
|
-
@property
|
|
35
|
-
def is_negative(self) -> bool:
|
|
36
|
-
return self == DataGenerationMethod.negative
|
|
37
|
-
|
|
38
|
-
@classmethod
|
|
39
|
-
def ensure_list(cls, value: DataGenerationMethodInput) -> list[DataGenerationMethod]:
|
|
40
|
-
if isinstance(value, DataGenerationMethod):
|
|
41
|
-
return [value]
|
|
42
|
-
return list(value)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
DataGenerationMethodInput = Union[DataGenerationMethod, Iterable[DataGenerationMethod]]
|
|
46
|
-
|
|
47
14
|
DEFAULT_DATA_GENERATION_METHODS = (DataGenerationMethod.default(),)
|
|
48
15
|
|
|
49
16
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import lru_cache, reduce
|
|
4
|
+
from operator import or_
|
|
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
|
+
|
|
12
|
+
@lru_cache()
|
|
13
|
+
def default_settings() -> settings:
|
|
14
|
+
from hypothesis import HealthCheck, Phase, Verbosity, settings
|
|
15
|
+
|
|
16
|
+
return settings(
|
|
17
|
+
database=None,
|
|
18
|
+
max_examples=1,
|
|
19
|
+
deadline=None,
|
|
20
|
+
verbosity=Verbosity.quiet,
|
|
21
|
+
phases=(Phase.generate,),
|
|
22
|
+
suppress_health_check=list(HealthCheck),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
T = TypeVar("T")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_single_example(strategy: st.SearchStrategy[T]) -> T: # type: ignore[type-var]
|
|
30
|
+
examples: list[T] = []
|
|
31
|
+
add_single_example(strategy, examples)
|
|
32
|
+
return examples[0]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def add_single_example(strategy: st.SearchStrategy[T], examples: list[T]) -> None:
|
|
36
|
+
from hypothesis import given
|
|
37
|
+
|
|
38
|
+
@given(strategy) # type: ignore
|
|
39
|
+
@default_settings() # type: ignore
|
|
40
|
+
def example_generating_inner_function(ex: T) -> None:
|
|
41
|
+
examples.append(ex)
|
|
42
|
+
|
|
43
|
+
example_generating_inner_function()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def combine_strategies(strategies: list[st.SearchStrategy] | tuple[st.SearchStrategy]) -> st.SearchStrategy:
|
|
47
|
+
"""Combine a list of strategies into a single one.
|
|
48
|
+
|
|
49
|
+
If the input is `[a, b, c]`, then the result is equivalent to `a | b | c`.
|
|
50
|
+
"""
|
|
51
|
+
return reduce(or_, strategies[1:], strategies[0])
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Iterable, Union
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DataGenerationMethod(str, Enum):
|
|
8
|
+
"""Defines what data Schemathesis generates for tests."""
|
|
9
|
+
|
|
10
|
+
# Generate data, that fits the API schema
|
|
11
|
+
positive = "positive"
|
|
12
|
+
# Doesn't fit the API schema
|
|
13
|
+
negative = "negative"
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def default(cls) -> DataGenerationMethod:
|
|
17
|
+
return cls.positive
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def all(cls) -> list[DataGenerationMethod]:
|
|
21
|
+
return list(DataGenerationMethod)
|
|
22
|
+
|
|
23
|
+
def as_short_name(self) -> str:
|
|
24
|
+
return {
|
|
25
|
+
DataGenerationMethod.positive: "P",
|
|
26
|
+
DataGenerationMethod.negative: "N",
|
|
27
|
+
}[self]
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def is_negative(self) -> bool:
|
|
31
|
+
return self == DataGenerationMethod.negative
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def ensure_list(cls, value: DataGenerationMethodInput) -> list[DataGenerationMethod]:
|
|
35
|
+
if isinstance(value, DataGenerationMethod):
|
|
36
|
+
return [value]
|
|
37
|
+
return list(value)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
DataGenerationMethodInput = Union[DataGenerationMethod, Iterable[DataGenerationMethod]]
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
from typing import Any, Generator, Set, TypeVar, cast
|
|
8
|
+
|
|
9
|
+
import jsonschema
|
|
10
|
+
from hypothesis import strategies as st
|
|
11
|
+
from hypothesis.errors import Unsatisfiable
|
|
12
|
+
from hypothesis_jsonschema import from_schema
|
|
13
|
+
from hypothesis_jsonschema._canonicalise import canonicalish
|
|
14
|
+
|
|
15
|
+
from ._hypothesis import combine_strategies, get_single_example
|
|
16
|
+
from ._methods import DataGenerationMethod
|
|
17
|
+
|
|
18
|
+
BUFFER_SIZE = 8 * 1024
|
|
19
|
+
FLOAT_STRATEGY: st.SearchStrategy = st.floats(allow_nan=False, allow_infinity=False).map(lambda x: x or 0.0)
|
|
20
|
+
NUMERIC_STRATEGY: st.SearchStrategy = st.integers() | FLOAT_STRATEGY
|
|
21
|
+
JSON_STRATEGY: st.SearchStrategy = st.recursive(
|
|
22
|
+
st.none() | st.booleans() | NUMERIC_STRATEGY | st.text(),
|
|
23
|
+
lambda strategy: st.lists(strategy, max_size=3) | st.dictionaries(st.text(), strategy, max_size=3),
|
|
24
|
+
)
|
|
25
|
+
ARRAY_STRATEGY: st.SearchStrategy = st.lists(JSON_STRATEGY)
|
|
26
|
+
OBJECT_STRATEGY: st.SearchStrategy = st.dictionaries(st.text(), JSON_STRATEGY)
|
|
27
|
+
|
|
28
|
+
UNKNOWN_PROPERTY_KEY = "x-schemathesis-unknown-property"
|
|
29
|
+
UNKNOWN_PROPERTY_VALUE = 42
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@lru_cache(maxsize=128)
|
|
33
|
+
def cached_draw(strategy: st.SearchStrategy) -> Any:
|
|
34
|
+
return get_single_example(strategy)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class CoverageContext:
|
|
39
|
+
data_generation_methods: list[DataGenerationMethod] = field(default_factory=DataGenerationMethod.all)
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def with_positive(cls) -> CoverageContext:
|
|
43
|
+
return CoverageContext(data_generation_methods=[DataGenerationMethod.positive])
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def with_negative(cls) -> CoverageContext:
|
|
47
|
+
return CoverageContext(data_generation_methods=[DataGenerationMethod.negative])
|
|
48
|
+
|
|
49
|
+
def generate_from(self, strategy: st.SearchStrategy, cached: bool = False) -> Any:
|
|
50
|
+
if cached:
|
|
51
|
+
value = cached_draw(strategy)
|
|
52
|
+
else:
|
|
53
|
+
value = get_single_example(strategy)
|
|
54
|
+
return value
|
|
55
|
+
|
|
56
|
+
def generate_from_schema(self, schema: dict) -> Any:
|
|
57
|
+
return self.generate_from(from_schema(schema))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
T = TypeVar("T")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _to_hashable_key(value: T) -> T | tuple[type, str]:
|
|
64
|
+
if isinstance(value, (dict, list)):
|
|
65
|
+
serialized = json.dumps(value, sort_keys=True)
|
|
66
|
+
return (type(value), serialized)
|
|
67
|
+
return value
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def cover_schema(ctx: CoverageContext, schema: dict) -> list:
|
|
71
|
+
return list(cover_schema_iter(ctx, schema))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _cover_positive_for_type(ctx: CoverageContext, schema: dict, ty: str | None) -> Generator:
|
|
75
|
+
if ty == "object" or ty == "array":
|
|
76
|
+
template_schema = _get_template_schema(schema, ty)
|
|
77
|
+
template = ctx.generate_from_schema(template_schema)
|
|
78
|
+
else:
|
|
79
|
+
template = None
|
|
80
|
+
if DataGenerationMethod.positive in ctx.data_generation_methods:
|
|
81
|
+
ctx = ctx.with_positive()
|
|
82
|
+
enum = schema.get("enum")
|
|
83
|
+
const = schema.get("const")
|
|
84
|
+
for key in ("anyOf", "oneOf"):
|
|
85
|
+
sub_schemas = schema.get(key)
|
|
86
|
+
if sub_schemas is not None:
|
|
87
|
+
for sub_schema in sub_schemas:
|
|
88
|
+
yield from cover_schema_iter(ctx, sub_schema)
|
|
89
|
+
all_of = schema.get("allOf")
|
|
90
|
+
if all_of is not None:
|
|
91
|
+
if len(all_of) == 1:
|
|
92
|
+
yield from cover_schema_iter(ctx, all_of[0])
|
|
93
|
+
else:
|
|
94
|
+
canonical = canonicalish(schema)
|
|
95
|
+
yield from cover_schema_iter(ctx, canonical)
|
|
96
|
+
if enum is not None:
|
|
97
|
+
yield from enum
|
|
98
|
+
elif const is not None:
|
|
99
|
+
yield const
|
|
100
|
+
elif ty is not None:
|
|
101
|
+
if ty == "null":
|
|
102
|
+
yield None
|
|
103
|
+
if ty == "boolean":
|
|
104
|
+
yield True
|
|
105
|
+
yield False
|
|
106
|
+
if ty == "string":
|
|
107
|
+
yield from _positive_string(ctx, schema)
|
|
108
|
+
if ty == "integer" or ty == "number":
|
|
109
|
+
yield from _positive_number(ctx, schema)
|
|
110
|
+
if ty == "array":
|
|
111
|
+
yield from _positive_array(ctx, schema, cast(list, template))
|
|
112
|
+
if ty == "object":
|
|
113
|
+
yield from _positive_object(ctx, schema, cast(dict, template))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def cover_schema_iter(ctx: CoverageContext, schema: dict) -> Generator:
|
|
117
|
+
types = schema.get("type", [])
|
|
118
|
+
if not isinstance(types, list):
|
|
119
|
+
types = [types]
|
|
120
|
+
if not types:
|
|
121
|
+
with suppress(Unsatisfiable, jsonschema.RefResolutionError):
|
|
122
|
+
yield from _cover_positive_for_type(ctx, schema, None)
|
|
123
|
+
for ty in types:
|
|
124
|
+
with suppress(Unsatisfiable, jsonschema.RefResolutionError):
|
|
125
|
+
yield from _cover_positive_for_type(ctx, schema, ty)
|
|
126
|
+
if DataGenerationMethod.negative in ctx.data_generation_methods:
|
|
127
|
+
template = None
|
|
128
|
+
seen: Set[Any | tuple[type, str]] = set()
|
|
129
|
+
for key, value in schema.items():
|
|
130
|
+
with suppress(Unsatisfiable, jsonschema.RefResolutionError):
|
|
131
|
+
if key == "enum":
|
|
132
|
+
yield from _negative_enum(ctx, value)
|
|
133
|
+
elif key == "const":
|
|
134
|
+
yield from _negative_enum(ctx, [value])
|
|
135
|
+
elif key == "type":
|
|
136
|
+
yield from _negative_type(ctx, seen, value)
|
|
137
|
+
elif key == "properties":
|
|
138
|
+
template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
|
|
139
|
+
yield from _negative_properties(ctx, template, value)
|
|
140
|
+
elif key == "pattern":
|
|
141
|
+
yield from _negative_pattern(ctx, value)
|
|
142
|
+
elif key == "format" and ("string" in types or not types):
|
|
143
|
+
yield from _negative_format(ctx, schema, value)
|
|
144
|
+
elif key == "maximum":
|
|
145
|
+
next = value + 1
|
|
146
|
+
yield next
|
|
147
|
+
seen.add(next)
|
|
148
|
+
elif key == "minimum":
|
|
149
|
+
next = value - 1
|
|
150
|
+
yield next
|
|
151
|
+
seen.add(next)
|
|
152
|
+
elif key == "exclusiveMaximum" or key == "exclusiveMinimum" and value not in seen:
|
|
153
|
+
yield value
|
|
154
|
+
seen.add(value)
|
|
155
|
+
elif key == "multipleOf":
|
|
156
|
+
yield from _negative_multiple_of(ctx, schema, value)
|
|
157
|
+
elif key == "minLength" and 0 < value < BUFFER_SIZE and "pattern" not in schema:
|
|
158
|
+
yield ctx.generate_from_schema({**schema, "minLength": value - 1, "maxLength": value - 1})
|
|
159
|
+
elif key == "maxLength" and value < BUFFER_SIZE and "pattern" not in schema:
|
|
160
|
+
yield ctx.generate_from_schema({**schema, "minLength": value + 1, "maxLength": value + 1})
|
|
161
|
+
elif key == "uniqueItems" and value:
|
|
162
|
+
yield from _negative_unique_items(ctx, schema)
|
|
163
|
+
elif key == "required":
|
|
164
|
+
template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
|
|
165
|
+
yield from _negative_required(ctx, template, value)
|
|
166
|
+
elif key == "additionalProperties" and not value:
|
|
167
|
+
template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
|
|
168
|
+
yield {**template, UNKNOWN_PROPERTY_KEY: UNKNOWN_PROPERTY_VALUE}
|
|
169
|
+
elif key == "allOf":
|
|
170
|
+
nctx = ctx.with_negative()
|
|
171
|
+
if len(value) == 1:
|
|
172
|
+
yield from cover_schema_iter(nctx, value[0])
|
|
173
|
+
else:
|
|
174
|
+
canonical = canonicalish(schema)
|
|
175
|
+
yield from cover_schema_iter(nctx, canonical)
|
|
176
|
+
elif key == "anyOf" or key == "oneOf":
|
|
177
|
+
nctx = ctx.with_negative()
|
|
178
|
+
# NOTE: Other sub-schemas are not filtered out
|
|
179
|
+
for sub_schema in value:
|
|
180
|
+
yield from cover_schema_iter(nctx, sub_schema)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _get_template_schema(schema: dict, ty: str) -> dict:
|
|
184
|
+
if ty == "object":
|
|
185
|
+
properties = schema.get("properties")
|
|
186
|
+
if properties is not None:
|
|
187
|
+
return {
|
|
188
|
+
**schema,
|
|
189
|
+
"required": list(properties),
|
|
190
|
+
"type": ty,
|
|
191
|
+
"properties": {
|
|
192
|
+
k: _get_template_schema(v, "object") if v.get("type") == "object" else v
|
|
193
|
+
for k, v in properties.items()
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
return {**schema, "type": ty}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _positive_string(ctx: CoverageContext, schema: dict) -> Generator:
|
|
200
|
+
"""Generate positive string values."""
|
|
201
|
+
# Boundary and near boundary values
|
|
202
|
+
min_length = schema.get("minLength")
|
|
203
|
+
max_length = schema.get("maxLength")
|
|
204
|
+
|
|
205
|
+
if not min_length and not max_length:
|
|
206
|
+
# Default positive value
|
|
207
|
+
yield ctx.generate_from_schema(schema)
|
|
208
|
+
|
|
209
|
+
seen = set()
|
|
210
|
+
|
|
211
|
+
if min_length is not None and min_length < BUFFER_SIZE and "pattern" not in schema:
|
|
212
|
+
# Exactly the minimum length
|
|
213
|
+
yield ctx.generate_from_schema({**schema, "maxLength": min_length})
|
|
214
|
+
seen.add(min_length)
|
|
215
|
+
|
|
216
|
+
# One character more than minimum if possible
|
|
217
|
+
larger = min_length + 1
|
|
218
|
+
if larger < BUFFER_SIZE and larger not in seen and (not max_length or larger <= max_length):
|
|
219
|
+
yield ctx.generate_from_schema({**schema, "minLength": larger, "maxLength": larger})
|
|
220
|
+
seen.add(larger)
|
|
221
|
+
|
|
222
|
+
if max_length is not None and "pattern" not in schema:
|
|
223
|
+
# Exactly the maximum length
|
|
224
|
+
if max_length < BUFFER_SIZE and max_length not in seen:
|
|
225
|
+
yield ctx.generate_from_schema({**schema, "minLength": max_length})
|
|
226
|
+
seen.add(max_length)
|
|
227
|
+
|
|
228
|
+
# One character less than maximum if possible
|
|
229
|
+
smaller = max_length - 1
|
|
230
|
+
if (
|
|
231
|
+
smaller < BUFFER_SIZE
|
|
232
|
+
and smaller not in seen
|
|
233
|
+
and (smaller > 0 and (min_length is None or smaller >= min_length))
|
|
234
|
+
):
|
|
235
|
+
yield ctx.generate_from_schema({**schema, "minLength": smaller, "maxLength": smaller})
|
|
236
|
+
seen.add(smaller)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def closest_multiple_greater_than(y: int, x: int) -> int:
|
|
240
|
+
"""Find the closest multiple of X that is greater than Y."""
|
|
241
|
+
quotient, remainder = divmod(y, x)
|
|
242
|
+
if remainder == 0:
|
|
243
|
+
return y
|
|
244
|
+
return x * (quotient + 1)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _positive_number(ctx: CoverageContext, schema: dict) -> Generator:
|
|
248
|
+
"""Generate positive integer values."""
|
|
249
|
+
# Boundary and near boundary values
|
|
250
|
+
minimum = schema.get("minimum")
|
|
251
|
+
maximum = schema.get("maximum")
|
|
252
|
+
exclusive_minimum = schema.get("exclusiveMinimum")
|
|
253
|
+
exclusive_maximum = schema.get("exclusiveMaximum")
|
|
254
|
+
if exclusive_minimum is not None:
|
|
255
|
+
minimum = exclusive_minimum + 1
|
|
256
|
+
if exclusive_maximum is not None:
|
|
257
|
+
maximum = exclusive_maximum - 1
|
|
258
|
+
multiple_of = schema.get("multipleOf")
|
|
259
|
+
|
|
260
|
+
if not minimum and not maximum:
|
|
261
|
+
# Default positive value
|
|
262
|
+
yield ctx.generate_from_schema(schema)
|
|
263
|
+
|
|
264
|
+
seen = set()
|
|
265
|
+
|
|
266
|
+
if minimum is not None:
|
|
267
|
+
# Exactly the minimum
|
|
268
|
+
if multiple_of is not None:
|
|
269
|
+
smallest = closest_multiple_greater_than(minimum, multiple_of)
|
|
270
|
+
else:
|
|
271
|
+
smallest = minimum
|
|
272
|
+
seen.add(smallest)
|
|
273
|
+
yield smallest
|
|
274
|
+
|
|
275
|
+
# One more than minimum if possible
|
|
276
|
+
if multiple_of is not None:
|
|
277
|
+
larger = smallest + multiple_of
|
|
278
|
+
else:
|
|
279
|
+
larger = minimum + 1
|
|
280
|
+
if larger not in seen and (not maximum or larger <= maximum):
|
|
281
|
+
seen.add(larger)
|
|
282
|
+
yield larger
|
|
283
|
+
|
|
284
|
+
if maximum is not None:
|
|
285
|
+
# Exactly the maximum
|
|
286
|
+
if multiple_of is not None:
|
|
287
|
+
largest = maximum - (maximum % multiple_of)
|
|
288
|
+
else:
|
|
289
|
+
largest = maximum
|
|
290
|
+
if largest not in seen:
|
|
291
|
+
seen.add(largest)
|
|
292
|
+
yield largest
|
|
293
|
+
|
|
294
|
+
# One less than maximum if possible
|
|
295
|
+
if multiple_of is not None:
|
|
296
|
+
smaller = largest - multiple_of
|
|
297
|
+
else:
|
|
298
|
+
smaller = maximum - 1
|
|
299
|
+
if smaller not in seen and (smaller > 0 and (minimum is None or smaller >= minimum)):
|
|
300
|
+
seen.add(smaller)
|
|
301
|
+
yield smaller
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Generator:
|
|
305
|
+
seen = set()
|
|
306
|
+
yield template
|
|
307
|
+
seen.add(len(template))
|
|
308
|
+
|
|
309
|
+
# Boundary and near-boundary sizes
|
|
310
|
+
min_items = schema.get("minItems")
|
|
311
|
+
max_items = schema.get("maxItems")
|
|
312
|
+
if min_items is not None:
|
|
313
|
+
# Do not generate an array with `minItems` length, because it is already covered by `template`
|
|
314
|
+
|
|
315
|
+
# One item more than minimum if possible
|
|
316
|
+
larger = min_items + 1
|
|
317
|
+
if larger not in seen and (max_items is None or larger <= max_items):
|
|
318
|
+
yield ctx.generate_from_schema({**schema, "minItems": larger, "maxItems": larger})
|
|
319
|
+
seen.add(larger)
|
|
320
|
+
|
|
321
|
+
if max_items is not None:
|
|
322
|
+
if max_items not in seen:
|
|
323
|
+
yield ctx.generate_from_schema({**schema, "minItems": max_items})
|
|
324
|
+
seen.add(max_items)
|
|
325
|
+
|
|
326
|
+
# One item smaller than maximum if possible
|
|
327
|
+
smaller = max_items - 1
|
|
328
|
+
if smaller > 0 and smaller not in seen and (min_items is None or smaller >= min_items):
|
|
329
|
+
yield ctx.generate_from_schema({**schema, "minItems": smaller, "maxItems": smaller})
|
|
330
|
+
seen.add(smaller)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Generator:
|
|
334
|
+
yield template
|
|
335
|
+
# Only required properties
|
|
336
|
+
properties = schema.get("properties", {})
|
|
337
|
+
if set(properties) != set(schema.get("required", {})):
|
|
338
|
+
only_required = {k: v for k, v in template.items() if k in schema.get("required", [])}
|
|
339
|
+
yield only_required
|
|
340
|
+
seen = set()
|
|
341
|
+
for name, sub_schema in properties.items():
|
|
342
|
+
seen.add(_to_hashable_key(template.get(name)))
|
|
343
|
+
for new in cover_schema_iter(ctx, sub_schema):
|
|
344
|
+
key = _to_hashable_key(new)
|
|
345
|
+
if key not in seen:
|
|
346
|
+
yield {**template, name: new}
|
|
347
|
+
seen.add(key)
|
|
348
|
+
seen.clear()
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@lru_cache(maxsize=128)
|
|
352
|
+
def _get_negative_enum_strategy(value: tuple) -> st.SearchStrategy:
|
|
353
|
+
return JSON_STRATEGY.filter(lambda x: x not in value)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _negative_enum(ctx: CoverageContext, value: list) -> Generator:
|
|
357
|
+
try:
|
|
358
|
+
strategy = _get_negative_enum_strategy(tuple(value))
|
|
359
|
+
except TypeError:
|
|
360
|
+
# The value is not hashable
|
|
361
|
+
strategy = JSON_STRATEGY.filter(lambda x: x not in value)
|
|
362
|
+
# The exact negative value is not important here
|
|
363
|
+
yield ctx.generate_from(strategy, cached=True)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _negative_properties(ctx: CoverageContext, template: dict, properties: dict) -> Generator:
|
|
367
|
+
nctx = ctx.with_negative()
|
|
368
|
+
for key, sub_schema in properties.items():
|
|
369
|
+
for value in cover_schema_iter(nctx, sub_schema):
|
|
370
|
+
yield {**template, key: value}
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@lru_cache(maxsize=128)
|
|
374
|
+
def _get_negative_pattern_strategy(value: str) -> st.SearchStrategy:
|
|
375
|
+
return st.text().filter(lambda x: x != value)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _negative_pattern(ctx: CoverageContext, pattern: str) -> Generator:
|
|
379
|
+
yield ctx.generate_from(_get_negative_pattern_strategy(pattern), cached=True)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _with_negated_key(schema: dict, key: str, value: Any) -> dict:
|
|
383
|
+
return {"allOf": [{k: v for k, v in schema.items() if k != key}, {"not": {key: value}}]}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _negative_multiple_of(ctx: CoverageContext, schema: dict, multiple_of: int | float) -> Generator:
|
|
387
|
+
yield ctx.generate_from_schema(_with_negated_key(schema, "multipleOf", multiple_of))
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _negative_unique_items(ctx: CoverageContext, schema: dict) -> Generator:
|
|
391
|
+
unique = ctx.generate_from_schema({**schema, "type": "array", "minItems": 1, "maxItems": 1})
|
|
392
|
+
yield unique + unique
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _negative_required(ctx: CoverageContext, template: dict, required: list[str]) -> Generator:
|
|
396
|
+
for key in required:
|
|
397
|
+
yield {k: v for k, v in template.items() if k != key}
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _negative_format(ctx: CoverageContext, schema: dict, format: str) -> Generator:
|
|
401
|
+
# Hypothesis-jsonschema does not canonicalise it properly right now, which leads to unsatisfiable schema
|
|
402
|
+
without_format = {k: v for k, v in schema.items() if k != "format"}
|
|
403
|
+
without_format.setdefault("type", "string")
|
|
404
|
+
strategy = from_schema(without_format)
|
|
405
|
+
if format in jsonschema.Draft202012Validator.FORMAT_CHECKER.checkers:
|
|
406
|
+
strategy = strategy.filter(lambda v: not jsonschema.Draft202012Validator.FORMAT_CHECKER.conforms(v, format))
|
|
407
|
+
yield ctx.generate_from(strategy)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Generator:
|
|
411
|
+
strategies = {
|
|
412
|
+
"integer": st.integers(),
|
|
413
|
+
"number": NUMERIC_STRATEGY,
|
|
414
|
+
"boolean": st.booleans(),
|
|
415
|
+
"null": st.none(),
|
|
416
|
+
"string": st.text(),
|
|
417
|
+
"array": ARRAY_STRATEGY,
|
|
418
|
+
"object": OBJECT_STRATEGY,
|
|
419
|
+
}
|
|
420
|
+
if isinstance(ty, str):
|
|
421
|
+
types = [ty]
|
|
422
|
+
else:
|
|
423
|
+
types = ty
|
|
424
|
+
for ty_ in types:
|
|
425
|
+
strategies.pop(ty_)
|
|
426
|
+
if "number" in types:
|
|
427
|
+
del strategies["integer"]
|
|
428
|
+
if "integer" in types:
|
|
429
|
+
strategies["number"] = FLOAT_STRATEGY.filter(lambda x: x != int(x))
|
|
430
|
+
negative_strategy = combine_strategies(tuple(strategies.values())).filter(lambda x: _to_hashable_key(x) not in seen)
|
|
431
|
+
value = ctx.generate_from(negative_strategy, cached=True)
|
|
432
|
+
yield value
|
|
433
|
+
seen.add(_to_hashable_key(value))
|
schemathesis/schemas.py
CHANGED
|
@@ -50,6 +50,7 @@ from .generation import (
|
|
|
50
50
|
DataGenerationMethod,
|
|
51
51
|
DataGenerationMethodInput,
|
|
52
52
|
GenerationConfig,
|
|
53
|
+
combine_strategies,
|
|
53
54
|
)
|
|
54
55
|
from .hooks import HookContext, HookDispatcher, HookScope, dispatch, to_filterable_hook
|
|
55
56
|
from .internal.deprecation import warn_filtration_arguments
|
|
@@ -69,7 +70,7 @@ from .types import (
|
|
|
69
70
|
PathParameters,
|
|
70
71
|
Query,
|
|
71
72
|
)
|
|
72
|
-
from .utils import PARAMETRIZE_MARKER, GivenInput,
|
|
73
|
+
from .utils import PARAMETRIZE_MARKER, GivenInput, given_proxy
|
|
73
74
|
|
|
74
75
|
if TYPE_CHECKING:
|
|
75
76
|
from .transports import Transport
|
|
@@ -128,7 +128,7 @@ def _apply_schema_patches_extension(extension: SchemaPatchesExtension, schema: B
|
|
|
128
128
|
|
|
129
129
|
|
|
130
130
|
def strategy_from_definitions(definitions: list[StrategyDefinition]) -> Result[st.SearchStrategy, Exception]:
|
|
131
|
-
from ..
|
|
131
|
+
from ..generation import combine_strategies
|
|
132
132
|
|
|
133
133
|
strategies = []
|
|
134
134
|
for definition in definitions:
|
|
@@ -10,8 +10,8 @@ import requests
|
|
|
10
10
|
from hypothesis.strategies import SearchStrategy
|
|
11
11
|
from hypothesis_jsonschema import from_schema
|
|
12
12
|
|
|
13
|
-
from ..._hypothesis import get_single_example
|
|
14
13
|
from ...constants import DEFAULT_RESPONSE_TIMEOUT
|
|
14
|
+
from ...generation import get_single_example
|
|
15
15
|
from ...internal.copy import fast_deepcopy
|
|
16
16
|
from ...models import APIOperation, Case
|
|
17
17
|
from ._hypothesis import get_case_strategy, get_default_format_strategies
|
|
@@ -1032,6 +1032,7 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
|
1032
1032
|
query: Query | None = None,
|
|
1033
1033
|
body: Body | NotSet = NOT_SET,
|
|
1034
1034
|
media_type: str | None = None,
|
|
1035
|
+
generation_time: float = 0.0,
|
|
1035
1036
|
) -> C:
|
|
1036
1037
|
if body is not NOT_SET and media_type is None:
|
|
1037
1038
|
media_type = operation._get_default_media_type()
|
|
@@ -1043,7 +1044,7 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
|
1043
1044
|
query=query,
|
|
1044
1045
|
body=body,
|
|
1045
1046
|
media_type=media_type,
|
|
1046
|
-
generation_time=
|
|
1047
|
+
generation_time=generation_time,
|
|
1047
1048
|
)
|
|
1048
1049
|
|
|
1049
1050
|
def _get_consumes_for_operation(self, definition: dict[str, Any]) -> list[str]:
|
|
@@ -8,11 +8,10 @@ from hypothesis import strategies as st
|
|
|
8
8
|
from hypothesis.stateful import Bundle, Rule, precondition, rule
|
|
9
9
|
|
|
10
10
|
from ....constants import NOT_SET
|
|
11
|
-
from ....generation import DataGenerationMethod
|
|
11
|
+
from ....generation import DataGenerationMethod, combine_strategies
|
|
12
12
|
from ....internal.result import Ok
|
|
13
13
|
from ....stateful.state_machine import APIStateMachine, Direction, StepResult
|
|
14
14
|
from ....types import NotSet
|
|
15
|
-
from ....utils import combine_strategies
|
|
16
15
|
from .. import expressions
|
|
17
16
|
from ..links import get_all_links
|
|
18
17
|
from ..utils import expand_status_code
|
schemathesis/utils.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import functools
|
|
4
|
-
import operator
|
|
5
4
|
from contextlib import contextmanager
|
|
6
5
|
from inspect import getfullargspec
|
|
7
6
|
from pathlib import Path
|
|
@@ -14,7 +13,6 @@ from typing import (
|
|
|
14
13
|
)
|
|
15
14
|
|
|
16
15
|
import pytest
|
|
17
|
-
from hypothesis import strategies as st
|
|
18
16
|
from hypothesis.core import is_invalid_test
|
|
19
17
|
from hypothesis.reporting import with_reporter
|
|
20
18
|
from hypothesis.strategies import SearchStrategy
|
|
@@ -149,14 +147,6 @@ def compose(*functions: Callable) -> Callable:
|
|
|
149
147
|
return functools.reduce(lambda f, g: lambda x: f(g(x)), functions, noop)
|
|
150
148
|
|
|
151
149
|
|
|
152
|
-
def combine_strategies(strategies: list[st.SearchStrategy]) -> st.SearchStrategy:
|
|
153
|
-
"""Combine a list of strategies into a single one.
|
|
154
|
-
|
|
155
|
-
If the input is `[a, b, c]`, then the result is equivalent to `a | b | c`.
|
|
156
|
-
"""
|
|
157
|
-
return functools.reduce(operator.or_, strategies[1:], strategies[0])
|
|
158
|
-
|
|
159
|
-
|
|
160
150
|
def skip(operation_name: str) -> NoReturn:
|
|
161
151
|
raise SkipTest(f"It is not possible to generate negative test cases for `{operation_name}`")
|
|
162
152
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: schemathesis
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.35.0
|
|
4
4
|
Summary: Property-based testing framework for Open API and GraphQL based apps
|
|
5
5
|
Project-URL: Documentation, https://schemathesis.readthedocs.io/en/stable/
|
|
6
6
|
Project-URL: Changelog, https://schemathesis.readthedocs.io/en/stable/changelog.html
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
schemathesis/__init__.py,sha256=pNaTfaC3NSdediNQuH9QAcuIx3U-MmSydvhS65FZrxw,1984
|
|
2
2
|
schemathesis/_compat.py,sha256=y4RZd59i2NCnZ91VQhnKeMn_8t3SgvLOk2Xm8nymUHY,1837
|
|
3
3
|
schemathesis/_dependency_versions.py,sha256=pjEkkGAfOQJYNb-9UOo84V8nj_lKHr_TGDVdFwY2UU0,816
|
|
4
|
-
schemathesis/_hypothesis.py,sha256=
|
|
4
|
+
schemathesis/_hypothesis.py,sha256=eLDbqhG2fBJ6DuTxXlRfadnHhFc3JE2P5zf0YS9UonQ,12782
|
|
5
5
|
schemathesis/_lazy_import.py,sha256=aMhWYgbU2JOltyWBb32vnWBb6kykOghucEzI_F70yVE,470
|
|
6
6
|
schemathesis/_override.py,sha256=3CbA7P9Q89W3ymaYxiOV5Xpv1yhoBqroLK4YRpYMjX4,1630
|
|
7
7
|
schemathesis/_rate_limiter.py,sha256=q_XWst5hzuAyXQRiZc4s_bx7-JlPYZM_yKDmeavt3oo,242
|
|
@@ -21,34 +21,34 @@ schemathesis/models.py,sha256=WpF-W592_MVWnMZ8t7wt_A2fr3YNLEwPX3r-jwW8GpY,44266
|
|
|
21
21
|
schemathesis/parameters.py,sha256=PndmqQRlEYsCt1kWjSShPsFf6vj7X_7FRdz_-A95eNg,2258
|
|
22
22
|
schemathesis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
23
|
schemathesis/sanitization.py,sha256=_qSt04f_XcHrgguyUnowvdfj-b6u409Ubu07i0ivQUQ,9011
|
|
24
|
-
schemathesis/schemas.py,sha256=
|
|
24
|
+
schemathesis/schemas.py,sha256=nkcGcWsRCaT3wgx9vxyvluehqkWur7Fuhv8JAAu9AhY,20614
|
|
25
25
|
schemathesis/serializers.py,sha256=kxXZ-UGa1v_vOm0sC4QYcrNv4rfvI7tHGT2elRVbCbc,11649
|
|
26
26
|
schemathesis/targets.py,sha256=XIGRghvEzbmEJjse9aZgNEj67L3jAbiazm2rxURWgDE,2351
|
|
27
27
|
schemathesis/throttling.py,sha256=aisUc4MJDGIOGUAs9L2DlWWpdd4KyAFuNVKhYoaUC9M,1719
|
|
28
28
|
schemathesis/types.py,sha256=xOzNAeMs6qqeaJnWs5Fpw5JPbvVjyfRfxTJa3G2Ln5I,920
|
|
29
|
-
schemathesis/utils.py,sha256=
|
|
30
|
-
schemathesis/cli/__init__.py,sha256=
|
|
29
|
+
schemathesis/utils.py,sha256=bYvB3l1iMxiUNHu7_1qhOj5gJf_8QUssL4Uoqgrux9A,4878
|
|
30
|
+
schemathesis/cli/__init__.py,sha256=O2VIpE9EWFrklWlVTTFUQGShxN8Ij9nbaOJFslDEmHY,73976
|
|
31
31
|
schemathesis/cli/__main__.py,sha256=MWaenjaUTZIfNPFzKmnkTiawUri7DVldtg3mirLwzU8,92
|
|
32
32
|
schemathesis/cli/callbacks.py,sha256=PJs64n6qGrGC5Yv_yl3fGm797cvN6pp2enFLmSljGKs,15127
|
|
33
33
|
schemathesis/cli/cassettes.py,sha256=lKKKKLjoVT8VnZXjCOCmDYhvLKn1XSrCXCMF9EeftHA,18498
|
|
34
34
|
schemathesis/cli/constants.py,sha256=wk-0GsoJIel8wFFerQ6Kf_6eAYUtIWkwMFwyAqv3yj4,1635
|
|
35
|
-
schemathesis/cli/context.py,sha256=
|
|
35
|
+
schemathesis/cli/context.py,sha256=6OYpSbeRobkdyDGvSj_2_zwEelM9K6fXpUCUjVC2sQM,2243
|
|
36
36
|
schemathesis/cli/debug.py,sha256=_YA-bX1ujHl4bqQDEum7M-I2XHBTEGbvgkhvcvKhmgU,658
|
|
37
|
-
schemathesis/cli/handlers.py,sha256=
|
|
37
|
+
schemathesis/cli/handlers.py,sha256=EXSAFe5TQlHANz1AVlSttfsoDT2oeaeFbqq1N7e2udw,467
|
|
38
38
|
schemathesis/cli/junitxml.py,sha256=FeQbgjYYwD0twk1QSbP3AivLPhHGwIuikQ6CN2Kw9ho,5050
|
|
39
39
|
schemathesis/cli/options.py,sha256=DY5PUzpUNyYgpcqqFTeZjmVUykpbaI9fbN44QIWNOfA,2559
|
|
40
40
|
schemathesis/cli/reporting.py,sha256=zNKaRAUfDZ23-gMtiWbnG2OBndkYlM7ljKvk0Mc_lqI,3589
|
|
41
41
|
schemathesis/cli/sanitization.py,sha256=Onw_NWZSom6XTVNJ5NHnC0PAhrYAcGzIXJbsBCzLkn4,1005
|
|
42
42
|
schemathesis/cli/output/__init__.py,sha256=AXaUzQ1nhQ-vXhW4-X-91vE2VQtEcCOrGtQXXNN55iQ,29
|
|
43
|
-
schemathesis/cli/output/default.py,sha256=
|
|
43
|
+
schemathesis/cli/output/default.py,sha256=VrV5I4PYaZPiKzbuJXmDL2PtcPUDoWxbb6pC1ShgjJ8,39508
|
|
44
44
|
schemathesis/cli/output/short.py,sha256=CL6-Apxr5tuZ3BL1vecV1MiRY1wDt21g0wiUwZu6mLM,2607
|
|
45
45
|
schemathesis/contrib/__init__.py,sha256=FH8NL8NXgSKBFOF8Jy_EB6T4CJEaiM-tmDhz16B2o4k,187
|
|
46
46
|
schemathesis/contrib/unique_data.py,sha256=_ElPRLNp0XhdETiZ-aplKln4hgU04jxR17kmjAEWf1I,1318
|
|
47
47
|
schemathesis/contrib/openapi/__init__.py,sha256=N-BoCzrLGq9aynubhmQBS-LJUBv1wPJcveMCJDhyQl4,217
|
|
48
|
-
schemathesis/contrib/openapi/fill_missing_examples.py,sha256=
|
|
48
|
+
schemathesis/contrib/openapi/fill_missing_examples.py,sha256=SL3LXG4khjGKneU3aBu1MGIhYtwRMjK77NH8L--9JBE,583
|
|
49
49
|
schemathesis/contrib/openapi/formats/__init__.py,sha256=OpHWPW8MkTLVv83QXPYY1HVLflhmSH49hSVefm1oVV0,111
|
|
50
50
|
schemathesis/contrib/openapi/formats/uuid.py,sha256=PG7aV0QAQnQ1zKmKiDK3cJue3Xy-TLGzyMeCB_RQbgk,391
|
|
51
|
-
schemathesis/experimental/__init__.py,sha256=
|
|
51
|
+
schemathesis/experimental/__init__.py,sha256=vza-jHjFAdyrnOBWVkiXyQi2Q7HoTUM9Zs3E7ozJmog,3161
|
|
52
52
|
schemathesis/extra/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
53
53
|
schemathesis/extra/_aiohttp.py,sha256=-bIY0ucv7pfK3gA9PHiO4n7ajtZJJM9pS3EY3cLau9c,957
|
|
54
54
|
schemathesis/extra/_flask.py,sha256=BHCbxBDoMlIvNS5sRNyrPN1NwHxiDQt0IsyfYwwoFes,289
|
|
@@ -57,7 +57,10 @@ schemathesis/extra/pytest_plugin.py,sha256=ymicV2NjmSzee0ccUUUjNEvb9ihCxxf_8M60g
|
|
|
57
57
|
schemathesis/fixups/__init__.py,sha256=RP5QYJVJhp8LXjhH89fCRaIVU26dHCy74jD9seoYMuc,967
|
|
58
58
|
schemathesis/fixups/fast_api.py,sha256=mn-KzBqnR8jl4W5fY-_ZySabMDMUnpzCIESMHnlvE1c,1304
|
|
59
59
|
schemathesis/fixups/utf8_bom.py,sha256=lWT9RNmJG8i-l5AXIpaCT3qCPUwRgzXPW3eoOjmZETA,745
|
|
60
|
-
schemathesis/generation/__init__.py,sha256=
|
|
60
|
+
schemathesis/generation/__init__.py,sha256=IzldWIswXBjCUaVInvXDoaXIrUbtZIU5cisnWcg2IX8,1609
|
|
61
|
+
schemathesis/generation/_hypothesis.py,sha256=Qel0mBsZV6tOEspRGfbJKFZevaMgHJjzY1F0Oo1bP_Y,1408
|
|
62
|
+
schemathesis/generation/_methods.py,sha256=jCK09f4sedDfePrS-6BIiE-CcEE8fJ4ZHxq1BHoTltQ,1101
|
|
63
|
+
schemathesis/generation/coverage.py,sha256=_g4jcuNTZ7emudQklcHcnkwNZeP1QBo1smNkGZHeCMo,17176
|
|
61
64
|
schemathesis/internal/__init__.py,sha256=93HcdG3LF0BbQKbCteOsFMa1w6nXl8yTmx87QLNJOik,161
|
|
62
65
|
schemathesis/internal/copy.py,sha256=DcL56z-d69kKR_5u8mlHvjSL1UTyUKNMAwexrwHFY1s,1031
|
|
63
66
|
schemathesis/internal/datetime.py,sha256=zPLBL0XXLNfP-KYel3H2m8pnsxjsA_4d-zTOhJg2EPQ,136
|
|
@@ -82,7 +85,7 @@ schemathesis/service/ci.py,sha256=3m9xSWFhrpWA6mI_8bXI7sspND9RSqXFM9UseO5nIiM,67
|
|
|
82
85
|
schemathesis/service/client.py,sha256=Mpm90O59GctKVw38OcV7F99a8VzsL_Z5AT-dDkn25hc,5218
|
|
83
86
|
schemathesis/service/constants.py,sha256=Q1bhtLRkmhso4KSVAtWl0u446Wlbk3ShOL3ZdbPoJOM,1502
|
|
84
87
|
schemathesis/service/events.py,sha256=zgELQFB-ir8yzK1G-a5BDOyxrISuXKUQ72fIBKIdgJY,1238
|
|
85
|
-
schemathesis/service/extensions.py,sha256=
|
|
88
|
+
schemathesis/service/extensions.py,sha256=uU87ewgtdq610TvCRELsgFqt0YLSFb3dAHXlB0ZDQ_w,7832
|
|
86
89
|
schemathesis/service/hosts.py,sha256=ad2Lxq9Zcc9PP-1eFLQnxen4ImglcGOH8n7CGG72NNg,3123
|
|
87
90
|
schemathesis/service/metadata.py,sha256=x2LeCED1mdPf-YQJmjY8xtcIKHfD1ap5V0BGl-UgqNo,2087
|
|
88
91
|
schemathesis/service/models.py,sha256=ihItUJ9CvH4TvmdfJY3W88NR82OODF8a3RD7WRXn6RM,6578
|
|
@@ -104,14 +107,14 @@ schemathesis/specs/openapi/checks.py,sha256=LiwoL5W_qK40j-JFSc9hfM8IGSszMBUWe71Y
|
|
|
104
107
|
schemathesis/specs/openapi/constants.py,sha256=JqM_FHOenqS_MuUE9sxVQ8Hnw0DNM8cnKDwCwPLhID4,783
|
|
105
108
|
schemathesis/specs/openapi/converter.py,sha256=TaYgc5BBHPdkN-n0lqpbeVgLu3eL3L8Wu3y_Vo3TJaQ,2800
|
|
106
109
|
schemathesis/specs/openapi/definitions.py,sha256=Z186F0gNBSCmPg-Kk7Q-n6XxEZHIOzgUyeqixlC62XE,94058
|
|
107
|
-
schemathesis/specs/openapi/examples.py,sha256=
|
|
110
|
+
schemathesis/specs/openapi/examples.py,sha256=aE1OsR4X6OBSgRnFD0mVB2aQ3mWe65xiL2YsLb6xkeU,16169
|
|
108
111
|
schemathesis/specs/openapi/formats.py,sha256=JmmkQWNAj5XreXb7Edgj4LADAf4m86YulR_Ec8evpJ4,1220
|
|
109
112
|
schemathesis/specs/openapi/links.py,sha256=DCOu14VOFqKYYFbQJHWICDpmTBzJfeP2v2FXBwW3vBI,17531
|
|
110
113
|
schemathesis/specs/openapi/loaders.py,sha256=AcpvTK8qdirSRcHcinCjQbwfSQSx448LAh_GvFML1C0,25515
|
|
111
114
|
schemathesis/specs/openapi/media_types.py,sha256=dNTxpRQbY3SubdVjh4Cjb38R6Bc9MF9BsRQwPD87x0g,1017
|
|
112
115
|
schemathesis/specs/openapi/parameters.py,sha256=_6vNCnPXcdxjfAQbykCRLHjvmTpu_02xDJghxDrGYr8,13611
|
|
113
116
|
schemathesis/specs/openapi/references.py,sha256=euxM02kQGMHh4Ss1jWjOY_gyw_HazafKITIsvOEiAvI,9831
|
|
114
|
-
schemathesis/specs/openapi/schemas.py,sha256=
|
|
117
|
+
schemathesis/specs/openapi/schemas.py,sha256=3pZEAU9sB9PT32z3BhJPsSSR0wVpJXL20-z9hMWS52s,53053
|
|
115
118
|
schemathesis/specs/openapi/security.py,sha256=nEhDB_SvEFldmfpa9uOQywfWN6DtXHKmgtwucJvfN5Q,7096
|
|
116
119
|
schemathesis/specs/openapi/serialization.py,sha256=5qGdFHZ3n80UlbSXrO_bkr4Al_7ci_Z3aSUjZczNDQY,11384
|
|
117
120
|
schemathesis/specs/openapi/utils.py,sha256=-TCu0hTrlwp2x5qHNp-TxiHRMeIZC9OBmlhLssjRIiQ,742
|
|
@@ -127,7 +130,7 @@ schemathesis/specs/openapi/negative/__init__.py,sha256=gw0w_9tVQf_MY5Df3_xTZFC4r
|
|
|
127
130
|
schemathesis/specs/openapi/negative/mutations.py,sha256=lLEN0GLxvPmZBQ3tHCznDSjmZ4yQiQxspjv1UpO4Kx0,19019
|
|
128
131
|
schemathesis/specs/openapi/negative/types.py,sha256=a7buCcVxNBG6ILBM3A7oNTAX0lyDseEtZndBuej8MbI,174
|
|
129
132
|
schemathesis/specs/openapi/negative/utils.py,sha256=ozcOIuASufLqZSgnKUACjX-EOZrrkuNdXX0SDnLoGYA,168
|
|
130
|
-
schemathesis/specs/openapi/stateful/__init__.py,sha256=
|
|
133
|
+
schemathesis/specs/openapi/stateful/__init__.py,sha256=N-Rg9kPuDFAYu8quZbiRe8P1Qd3jz3wjMsIN19nSrhk,9449
|
|
131
134
|
schemathesis/specs/openapi/stateful/statistic.py,sha256=EJK4NqeAYRYl1FtU9YEuTLyhGhPmks0bLoxUPuQlOvM,7443
|
|
132
135
|
schemathesis/specs/openapi/stateful/types.py,sha256=UuGcCTFvaHsqeLN9ZeUNcbjsEwmthoT3UcHfDHchOYo,419
|
|
133
136
|
schemathesis/stateful/__init__.py,sha256=gONzl3pgZ8DihjK52Wd3Ye1LeP6gnulmevHI_jEqHWI,5088
|
|
@@ -144,8 +147,8 @@ schemathesis/transports/auth.py,sha256=yELjkEkfx4g74hNrd0Db9aFf0xDJDRIwhg2vzKOTZ
|
|
|
144
147
|
schemathesis/transports/content_types.py,sha256=VrcRQvF5T_TUjrCyrZcYF2LOwKfs3IrLcMtkVSp1ImI,2189
|
|
145
148
|
schemathesis/transports/headers.py,sha256=hr_AIDOfUxsJxpHfemIZ_uNG3_vzS_ZeMEKmZjbYiBE,990
|
|
146
149
|
schemathesis/transports/responses.py,sha256=6-gvVcRK0Ho_lSydUysBNFWoJwZEiEgf6Iv-GWkQGd8,1675
|
|
147
|
-
schemathesis-3.
|
|
148
|
-
schemathesis-3.
|
|
149
|
-
schemathesis-3.
|
|
150
|
-
schemathesis-3.
|
|
151
|
-
schemathesis-3.
|
|
150
|
+
schemathesis-3.35.0.dist-info/METADATA,sha256=hljiaQNfbUOdt0Dt5mIuOUzyd_Qn4gkFJDWhW2gIev8,19287
|
|
151
|
+
schemathesis-3.35.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
152
|
+
schemathesis-3.35.0.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
|
|
153
|
+
schemathesis-3.35.0.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
|
|
154
|
+
schemathesis-3.35.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|