schemathesis 3.34.3__py3-none-any.whl → 3.35.1__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 +82 -27
- schemathesis/cli/__init__.py +25 -4
- schemathesis/cli/cassettes.py +2 -0
- 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 +487 -0
- schemathesis/models.py +14 -1
- schemathesis/runner/serialization.py +3 -1
- schemathesis/schemas.py +2 -1
- schemathesis/service/extensions.py +1 -1
- schemathesis/specs/openapi/_hypothesis.py +3 -1
- schemathesis/specs/openapi/examples.py +4 -2
- 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.1.dist-info}/METADATA +1 -1
- {schemathesis-3.34.3.dist-info → schemathesis-3.35.1.dist-info}/RECORD +26 -23
- {schemathesis-3.34.3.dist-info → schemathesis-3.35.1.dist-info}/WHEEL +0 -0
- {schemathesis-3.34.3.dist-info → schemathesis-3.35.1.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.34.3.dist-info → schemathesis-3.35.1.dist-info}/licenses/LICENSE +0 -0
schemathesis/_hypothesis.py
CHANGED
|
@@ -3,26 +3,28 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import json
|
|
6
7
|
import warnings
|
|
7
8
|
from typing import Any, Callable, Generator, Mapping, Optional, Tuple
|
|
8
9
|
|
|
9
10
|
import hypothesis
|
|
10
11
|
from hypothesis import Phase
|
|
11
|
-
from hypothesis import strategies as st
|
|
12
12
|
from hypothesis.errors import HypothesisWarning, Unsatisfiable
|
|
13
13
|
from hypothesis.internal.entropy import deterministic_PRNG
|
|
14
14
|
from hypothesis.internal.reflection import proxies
|
|
15
15
|
from jsonschema.exceptions import SchemaError
|
|
16
16
|
|
|
17
17
|
from .auths import get_auth_storage_from_test
|
|
18
|
-
from .constants import DEFAULT_DEADLINE
|
|
18
|
+
from .constants import DEFAULT_DEADLINE, NOT_SET
|
|
19
19
|
from .exceptions import OperationSchemaError, SerializationNotPossible
|
|
20
|
-
from .
|
|
20
|
+
from .experimental import COVERAGE_PHASE
|
|
21
|
+
from .generation import DataGenerationMethod, GenerationConfig, combine_strategies, coverage, get_single_example
|
|
21
22
|
from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher
|
|
22
|
-
from .models import APIOperation, Case
|
|
23
|
+
from .models import APIOperation, Case, GenerationMetadata, TestPhase
|
|
23
24
|
from .transports.content_types import parse_content_type
|
|
24
25
|
from .transports.headers import has_invalid_characters, is_latin_1_encodable
|
|
25
|
-
from .
|
|
26
|
+
from .types import NotSet
|
|
27
|
+
from .utils import GivenInput
|
|
26
28
|
|
|
27
29
|
# Forcefully initializes Hypothesis' global PRNG to avoid races that initilize it
|
|
28
30
|
# if e.g. Schemathesis CLI is used with multiple workers
|
|
@@ -101,6 +103,8 @@ def create_test(
|
|
|
101
103
|
wrapped_test = add_examples(
|
|
102
104
|
wrapped_test, operation, hook_dispatcher=hook_dispatcher, as_strategy_kwargs=as_strategy_kwargs
|
|
103
105
|
)
|
|
106
|
+
if COVERAGE_PHASE.is_enabled:
|
|
107
|
+
wrapped_test = add_coverage(wrapped_test, operation, data_generation_methods)
|
|
104
108
|
return wrapped_test
|
|
105
109
|
|
|
106
110
|
|
|
@@ -199,6 +203,79 @@ def add_examples(
|
|
|
199
203
|
return test
|
|
200
204
|
|
|
201
205
|
|
|
206
|
+
def add_coverage(
|
|
207
|
+
test: Callable, operation: APIOperation, data_generation_methods: list[DataGenerationMethod]
|
|
208
|
+
) -> Callable:
|
|
209
|
+
for example in _iter_coverage_cases(operation, data_generation_methods):
|
|
210
|
+
test = hypothesis.example(case=example)(test)
|
|
211
|
+
return test
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _iter_coverage_cases(
|
|
215
|
+
operation: APIOperation, data_generation_methods: list[DataGenerationMethod]
|
|
216
|
+
) -> Generator[Case, None, None]:
|
|
217
|
+
from .specs.openapi.constants import LOCATION_TO_CONTAINER
|
|
218
|
+
|
|
219
|
+
ctx = coverage.CoverageContext(data_generation_methods=data_generation_methods)
|
|
220
|
+
meta = GenerationMetadata(
|
|
221
|
+
query=None, path_parameters=None, headers=None, cookies=None, body=None, phase=TestPhase.COVERAGE
|
|
222
|
+
)
|
|
223
|
+
generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
|
|
224
|
+
template: dict[str, Any] = {}
|
|
225
|
+
template_generation_method = DataGenerationMethod.positive
|
|
226
|
+
for parameter in operation.iter_parameters():
|
|
227
|
+
schema = parameter.as_json_schema(operation)
|
|
228
|
+
gen = coverage.cover_schema_iter(ctx, schema)
|
|
229
|
+
value = next(gen, NOT_SET)
|
|
230
|
+
if isinstance(value, NotSet):
|
|
231
|
+
continue
|
|
232
|
+
location = parameter.location
|
|
233
|
+
name = parameter.name
|
|
234
|
+
container = template.setdefault(LOCATION_TO_CONTAINER[location], {})
|
|
235
|
+
if location in ("header", "cookie") and not isinstance(value.value, str):
|
|
236
|
+
container[name] = json.dumps(value.value)
|
|
237
|
+
else:
|
|
238
|
+
container[name] = value.value
|
|
239
|
+
template_generation_method = value.data_generation_method
|
|
240
|
+
generators[(location, name)] = gen
|
|
241
|
+
if operation.body:
|
|
242
|
+
for body in operation.body:
|
|
243
|
+
schema = body.as_json_schema(operation)
|
|
244
|
+
gen = coverage.cover_schema_iter(ctx, schema)
|
|
245
|
+
value = next(gen, NOT_SET)
|
|
246
|
+
if isinstance(value, NotSet):
|
|
247
|
+
continue
|
|
248
|
+
if "body" not in template:
|
|
249
|
+
template["body"] = value.value
|
|
250
|
+
template["media_type"] = body.media_type
|
|
251
|
+
case = operation.make_case(**{**template, "body": value.value, "media_type": body.media_type})
|
|
252
|
+
case.data_generation_method = value.data_generation_method
|
|
253
|
+
case.meta = meta
|
|
254
|
+
yield case
|
|
255
|
+
for next_value in gen:
|
|
256
|
+
case = operation.make_case(**{**template, "body": next_value.value, "media_type": body.media_type})
|
|
257
|
+
case.data_generation_method = next_value.data_generation_method
|
|
258
|
+
case.meta = meta
|
|
259
|
+
yield case
|
|
260
|
+
else:
|
|
261
|
+
case = operation.make_case(**template)
|
|
262
|
+
case.data_generation_method = template_generation_method
|
|
263
|
+
case.meta = meta
|
|
264
|
+
yield case
|
|
265
|
+
for (location, name), gen in generators.items():
|
|
266
|
+
container_name = LOCATION_TO_CONTAINER[location]
|
|
267
|
+
container = template[container_name]
|
|
268
|
+
for value in gen:
|
|
269
|
+
if location in ("header", "cookie") and not isinstance(value.value, str):
|
|
270
|
+
generated = json.dumps(value.value)
|
|
271
|
+
else:
|
|
272
|
+
generated = value.value
|
|
273
|
+
case = operation.make_case(**{**template, container_name: {**container, name: generated}})
|
|
274
|
+
case.data_generation_method = value.data_generation_method
|
|
275
|
+
case.meta = meta
|
|
276
|
+
yield case
|
|
277
|
+
|
|
278
|
+
|
|
202
279
|
def find_invalid_headers(headers: Mapping) -> Generator[Tuple[str, str], None, None]:
|
|
203
280
|
for name, value in headers.items():
|
|
204
281
|
if not is_latin_1_encodable(value) or has_invalid_characters(name, value):
|
|
@@ -248,25 +325,3 @@ def get_invalid_example_headers_mark(test: Callable) -> Optional[dict[str, str]]
|
|
|
248
325
|
|
|
249
326
|
def add_invalid_example_header_mark(test: Callable, headers: dict[str, str]) -> None:
|
|
250
327
|
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/cassettes.py
CHANGED
|
@@ -226,6 +226,7 @@ http_interactions:"""
|
|
|
226
226
|
for interaction in item.interactions:
|
|
227
227
|
status = interaction.status.name.upper()
|
|
228
228
|
# Body payloads are handled via separate `stream.write` calls to avoid some allocations
|
|
229
|
+
phase = f"'{interaction.phase.value}'" if interaction.phase is not None else "null"
|
|
229
230
|
stream.write(
|
|
230
231
|
f"""\n- id: '{current_id}'
|
|
231
232
|
status: '{status}'
|
|
@@ -233,6 +234,7 @@ http_interactions:"""
|
|
|
233
234
|
thread_id: {item.thread_id}
|
|
234
235
|
correlation_id: '{item.correlation_id}'
|
|
235
236
|
data_generation_method: '{interaction.data_generation_method.value}'
|
|
237
|
+
phase: {phase}
|
|
236
238
|
elapsed: '{interaction.response.elapsed}'
|
|
237
239
|
recorded_at: '{interaction.recorded_at}'
|
|
238
240
|
checks:
|
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]]
|