schemathesis 3.35.4__py3-none-any.whl → 3.36.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +5 -5
- schemathesis/_hypothesis.py +12 -6
- schemathesis/_override.py +4 -4
- schemathesis/auths.py +1 -1
- schemathesis/checks.py +8 -5
- schemathesis/cli/__init__.py +23 -26
- schemathesis/cli/callbacks.py +6 -4
- schemathesis/cli/cassettes.py +67 -41
- schemathesis/cli/context.py +7 -6
- schemathesis/cli/junitxml.py +1 -1
- schemathesis/cli/options.py +7 -4
- schemathesis/cli/output/default.py +5 -5
- schemathesis/cli/reporting.py +4 -2
- schemathesis/code_samples.py +4 -3
- schemathesis/contrib/unique_data.py +1 -2
- schemathesis/exceptions.py +4 -3
- schemathesis/extra/_flask.py +4 -1
- schemathesis/extra/pytest_plugin.py +6 -3
- schemathesis/failures.py +2 -1
- schemathesis/filters.py +2 -2
- schemathesis/generation/__init__.py +2 -2
- schemathesis/generation/_hypothesis.py +1 -1
- schemathesis/generation/coverage.py +53 -12
- schemathesis/graphql.py +0 -1
- schemathesis/hooks.py +3 -3
- schemathesis/internal/checks.py +53 -0
- schemathesis/lazy.py +10 -7
- schemathesis/loaders.py +3 -3
- schemathesis/models.py +59 -23
- schemathesis/runner/__init__.py +12 -6
- schemathesis/runner/events.py +1 -1
- schemathesis/runner/impl/context.py +72 -0
- schemathesis/runner/impl/core.py +105 -67
- schemathesis/runner/impl/solo.py +17 -20
- schemathesis/runner/impl/threadpool.py +65 -72
- schemathesis/runner/serialization.py +4 -3
- schemathesis/sanitization.py +2 -1
- schemathesis/schemas.py +20 -22
- schemathesis/serializers.py +2 -0
- schemathesis/service/client.py +1 -1
- schemathesis/service/events.py +4 -1
- schemathesis/service/extensions.py +2 -2
- schemathesis/service/hosts.py +4 -2
- schemathesis/service/models.py +3 -3
- schemathesis/service/report.py +3 -3
- schemathesis/service/serialization.py +4 -2
- schemathesis/specs/graphql/loaders.py +5 -4
- schemathesis/specs/graphql/schemas.py +13 -8
- schemathesis/specs/openapi/checks.py +76 -27
- schemathesis/specs/openapi/definitions.py +1 -5
- schemathesis/specs/openapi/examples.py +92 -2
- schemathesis/specs/openapi/expressions/__init__.py +7 -0
- schemathesis/specs/openapi/expressions/extractors.py +4 -1
- schemathesis/specs/openapi/expressions/nodes.py +5 -3
- schemathesis/specs/openapi/links.py +4 -4
- schemathesis/specs/openapi/loaders.py +6 -5
- schemathesis/specs/openapi/negative/__init__.py +5 -3
- schemathesis/specs/openapi/negative/mutations.py +5 -4
- schemathesis/specs/openapi/parameters.py +4 -2
- schemathesis/specs/openapi/schemas.py +28 -13
- schemathesis/specs/openapi/security.py +6 -4
- schemathesis/specs/openapi/stateful/__init__.py +2 -2
- schemathesis/specs/openapi/stateful/statistic.py +3 -3
- schemathesis/specs/openapi/stateful/types.py +3 -2
- schemathesis/stateful/__init__.py +3 -3
- schemathesis/stateful/config.py +2 -1
- schemathesis/stateful/context.py +13 -3
- schemathesis/stateful/events.py +3 -3
- schemathesis/stateful/runner.py +24 -6
- schemathesis/stateful/sink.py +1 -1
- schemathesis/stateful/state_machine.py +7 -6
- schemathesis/stateful/statistic.py +3 -1
- schemathesis/stateful/validation.py +10 -5
- schemathesis/transports/__init__.py +2 -2
- schemathesis/transports/asgi.py +7 -0
- schemathesis/transports/auth.py +2 -1
- schemathesis/transports/content_types.py +1 -1
- schemathesis/transports/responses.py +2 -1
- schemathesis/utils.py +4 -2
- {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/METADATA +1 -1
- schemathesis-3.36.0.dist-info/RECORD +157 -0
- schemathesis-3.35.4.dist-info/RECORD +0 -154
- {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/WHEEL +0 -0
- {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/licenses/LICENSE +0 -0
schemathesis/__init__.py
CHANGED
|
@@ -2,12 +2,12 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
|
-
from . import auths, checks, contrib, experimental, fixups, graphql, hooks, runner, serializers, targets
|
|
5
|
+
from . import auths, checks, contrib, experimental, fixups, graphql, hooks, runner, serializers, targets
|
|
6
6
|
from ._lazy_import import lazy_import
|
|
7
|
-
from .constants import SCHEMATHESIS_VERSION
|
|
8
|
-
from .generation import DataGenerationMethod, GenerationConfig, HeaderConfig
|
|
9
|
-
from .models import Case
|
|
10
|
-
from .specs import openapi
|
|
7
|
+
from .constants import SCHEMATHESIS_VERSION
|
|
8
|
+
from .generation import DataGenerationMethod, GenerationConfig, HeaderConfig
|
|
9
|
+
from .models import Case
|
|
10
|
+
from .specs import openapi
|
|
11
11
|
|
|
12
12
|
__version__ = SCHEMATHESIS_VERSION
|
|
13
13
|
|
schemathesis/_hypothesis.py
CHANGED
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import json
|
|
7
7
|
import warnings
|
|
8
|
-
from typing import Any, Callable, Generator, Mapping
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Callable, Generator, Mapping
|
|
9
9
|
|
|
10
10
|
import hypothesis
|
|
11
11
|
from hypothesis import Phase
|
|
@@ -24,7 +24,9 @@ from .models import APIOperation, Case, GenerationMetadata, TestPhase
|
|
|
24
24
|
from .transports.content_types import parse_content_type
|
|
25
25
|
from .transports.headers import has_invalid_characters, is_latin_1_encodable
|
|
26
26
|
from .types import NotSet
|
|
27
|
-
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from .utils import GivenInput
|
|
28
30
|
|
|
29
31
|
# Forcefully initializes Hypothesis' global PRNG to avoid races that initilize it
|
|
30
32
|
# if e.g. Schemathesis CLI is used with multiple workers
|
|
@@ -215,6 +217,7 @@ def _iter_coverage_cases(
|
|
|
215
217
|
operation: APIOperation, data_generation_methods: list[DataGenerationMethod]
|
|
216
218
|
) -> Generator[Case, None, None]:
|
|
217
219
|
from .specs.openapi.constants import LOCATION_TO_CONTAINER
|
|
220
|
+
from .specs.openapi.examples import find_in_responses, find_matching_in_responses
|
|
218
221
|
|
|
219
222
|
ctx = coverage.CoverageContext(data_generation_methods=data_generation_methods)
|
|
220
223
|
meta = GenerationMetadata(
|
|
@@ -222,8 +225,11 @@ def _iter_coverage_cases(
|
|
|
222
225
|
)
|
|
223
226
|
generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
|
|
224
227
|
template: dict[str, Any] = {}
|
|
228
|
+
responses = find_in_responses(operation)
|
|
225
229
|
for parameter in operation.iter_parameters():
|
|
226
230
|
schema = parameter.as_json_schema(operation)
|
|
231
|
+
for value in find_matching_in_responses(responses, parameter.name):
|
|
232
|
+
schema.setdefault("examples", []).append(value)
|
|
227
233
|
gen = coverage.cover_schema_iter(ctx, schema)
|
|
228
234
|
value = next(gen, NOT_SET)
|
|
229
235
|
if isinstance(value, NotSet):
|
|
@@ -274,7 +280,7 @@ def _iter_coverage_cases(
|
|
|
274
280
|
yield case
|
|
275
281
|
|
|
276
282
|
|
|
277
|
-
def find_invalid_headers(headers: Mapping) -> Generator[
|
|
283
|
+
def find_invalid_headers(headers: Mapping) -> Generator[tuple[str, str], None, None]:
|
|
278
284
|
for name, value in headers.items():
|
|
279
285
|
if not is_latin_1_encodable(value) or has_invalid_characters(name, value):
|
|
280
286
|
yield name, value
|
|
@@ -305,11 +311,11 @@ def add_non_serializable_mark(test: Callable, exc: SerializationNotPossible) ->
|
|
|
305
311
|
test._schemathesis_non_serializable = exc # type: ignore
|
|
306
312
|
|
|
307
313
|
|
|
308
|
-
def get_non_serializable_mark(test: Callable) ->
|
|
314
|
+
def get_non_serializable_mark(test: Callable) -> SerializationNotPossible | None:
|
|
309
315
|
return getattr(test, "_schemathesis_non_serializable", None)
|
|
310
316
|
|
|
311
317
|
|
|
312
|
-
def get_invalid_regex_mark(test: Callable) ->
|
|
318
|
+
def get_invalid_regex_mark(test: Callable) -> SchemaError | None:
|
|
313
319
|
return getattr(test, "_schemathesis_invalid_regex", None)
|
|
314
320
|
|
|
315
321
|
|
|
@@ -317,7 +323,7 @@ def add_invalid_regex_mark(test: Callable, exc: SchemaError) -> None:
|
|
|
317
323
|
test._schemathesis_invalid_regex = exc # type: ignore
|
|
318
324
|
|
|
319
325
|
|
|
320
|
-
def get_invalid_example_headers_mark(test: Callable) ->
|
|
326
|
+
def get_invalid_example_headers_mark(test: Callable) -> dict[str, str] | None:
|
|
321
327
|
return getattr(test, "_schemathesis_invalid_example_headers", None)
|
|
322
328
|
|
|
323
329
|
|
schemathesis/_override.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
5
|
|
|
6
6
|
from .exceptions import UsageError
|
|
7
|
-
from .parameters import ParameterSet
|
|
8
|
-
from .types import GenericTest
|
|
9
7
|
|
|
10
8
|
if TYPE_CHECKING:
|
|
11
9
|
from .models import APIOperation
|
|
10
|
+
from .parameters import ParameterSet
|
|
11
|
+
from .types import GenericTest
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
@dataclass
|
|
@@ -37,7 +37,7 @@ def _for_parameters(overridden: dict[str, str], defined: ParameterSet) -> dict[s
|
|
|
37
37
|
return output
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
def get_override_from_mark(test: GenericTest) ->
|
|
40
|
+
def get_override_from_mark(test: GenericTest) -> CaseOverride | None:
|
|
41
41
|
return getattr(test, "_schemathesis_override", None)
|
|
42
42
|
|
|
43
43
|
|
schemathesis/auths.py
CHANGED
|
@@ -21,12 +21,12 @@ from typing import (
|
|
|
21
21
|
|
|
22
22
|
from .exceptions import UsageError
|
|
23
23
|
from .filters import FilterSet, FilterValue, MatcherFunc, attach_filter_chain
|
|
24
|
-
from .types import GenericTest
|
|
25
24
|
|
|
26
25
|
if TYPE_CHECKING:
|
|
27
26
|
import requests.auth
|
|
28
27
|
|
|
29
28
|
from .models import APIOperation, Case
|
|
29
|
+
from .types import GenericTest
|
|
30
30
|
|
|
31
31
|
DEFAULT_REFRESH_INTERVAL = 300
|
|
32
32
|
AUTH_STORAGE_ATTRIBUTE_NAME = "_schemathesis_auth"
|
schemathesis/checks.py
CHANGED
|
@@ -15,11 +15,12 @@ from .specs.openapi.checks import (
|
|
|
15
15
|
)
|
|
16
16
|
|
|
17
17
|
if TYPE_CHECKING:
|
|
18
|
-
from .
|
|
18
|
+
from .internal.checks import CheckContext, CheckFunction
|
|
19
|
+
from .models import Case
|
|
19
20
|
from .transports.responses import GenericResponse
|
|
20
21
|
|
|
21
22
|
|
|
22
|
-
def not_a_server_error(response: GenericResponse, case: Case) -> bool | None:
|
|
23
|
+
def not_a_server_error(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
|
|
23
24
|
"""A check to verify that the response is not a server-side error."""
|
|
24
25
|
from .specs.graphql.schemas import GraphQLCase
|
|
25
26
|
from .specs.graphql.validation import validate_graphql_response
|
|
@@ -64,14 +65,16 @@ def register(check: CheckFunction) -> CheckFunction:
|
|
|
64
65
|
.. code-block:: python
|
|
65
66
|
|
|
66
67
|
@schemathesis.check
|
|
67
|
-
def new_check(response, case):
|
|
68
|
+
def new_check(ctx, response, case):
|
|
68
69
|
# some awesome assertions!
|
|
69
70
|
...
|
|
70
71
|
"""
|
|
71
72
|
from . import cli
|
|
73
|
+
from .internal.checks import wrap_check
|
|
72
74
|
|
|
75
|
+
_check = wrap_check(check)
|
|
73
76
|
global ALL_CHECKS
|
|
74
77
|
|
|
75
|
-
ALL_CHECKS += (
|
|
76
|
-
cli.CHECKS_TYPE.choices += (
|
|
78
|
+
ALL_CHECKS += (_check,)
|
|
79
|
+
cli.CHECKS_TYPE.choices += (_check.__name__,) # type: ignore
|
|
77
80
|
return check
|
schemathesis/cli/__init__.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import base64
|
|
4
|
-
import io
|
|
5
4
|
import os
|
|
6
5
|
import sys
|
|
7
6
|
import traceback
|
|
@@ -41,38 +40,45 @@ from ..internal.datetime import current_datetime
|
|
|
41
40
|
from ..internal.output import OutputConfig
|
|
42
41
|
from ..internal.validation import file_exists
|
|
43
42
|
from ..loaders import load_app, load_yaml
|
|
44
|
-
from ..models import Case, CheckFunction
|
|
45
43
|
from ..runner import events, prepare_hypothesis_settings, probes
|
|
46
44
|
from ..specs.graphql import loaders as gql_loaders
|
|
47
45
|
from ..specs.openapi import loaders as oas_loaders
|
|
48
46
|
from ..stateful import Stateful
|
|
49
|
-
from ..targets import Target
|
|
50
47
|
from ..transports import RequestConfig
|
|
51
48
|
from ..transports.auth import get_requests_auth
|
|
52
|
-
from ..types import PathLike, RequestCert
|
|
53
49
|
from . import callbacks, cassettes, output
|
|
54
50
|
from .constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS, HealthCheck, Phase, Verbosity
|
|
55
51
|
from .context import ExecutionContext, FileReportContext, ServiceReportContext
|
|
56
52
|
from .debug import DebugOutputHandler
|
|
57
53
|
from .handlers import EventHandler
|
|
58
54
|
from .junitxml import JunitXMLHandler
|
|
59
|
-
from .options import CsvChoice, CsvEnumChoice, CustomHelpMessageChoice,
|
|
55
|
+
from .options import CsvChoice, CsvEnumChoice, CustomHelpMessageChoice, OptionalInt
|
|
60
56
|
from .sanitization import SanitizationHandler
|
|
61
57
|
|
|
62
58
|
if TYPE_CHECKING:
|
|
59
|
+
import io
|
|
60
|
+
|
|
63
61
|
import hypothesis
|
|
64
62
|
import requests
|
|
65
63
|
|
|
64
|
+
from ..models import Case, CheckFunction
|
|
66
65
|
from ..schemas import BaseSchema
|
|
67
66
|
from ..service.client import ServiceClient
|
|
68
67
|
from ..specs.graphql.schemas import GraphQLSchema
|
|
68
|
+
from ..targets import Target
|
|
69
|
+
from ..types import NotSet, PathLike, RequestCert
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
__all__ = [
|
|
73
|
+
"EventHandler",
|
|
74
|
+
]
|
|
69
75
|
|
|
70
76
|
|
|
71
77
|
def _get_callable_names(items: tuple[Callable, ...]) -> tuple[str, ...]:
|
|
72
78
|
return tuple(item.__name__ for item in items)
|
|
73
79
|
|
|
74
80
|
|
|
75
|
-
CUSTOM_HANDLERS: list[
|
|
81
|
+
CUSTOM_HANDLERS: list[type[EventHandler]] = []
|
|
76
82
|
CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
|
|
77
83
|
|
|
78
84
|
DEFAULT_CHECKS_NAMES = _get_callable_names(checks_module.DEFAULT_CHECKS)
|
|
@@ -98,13 +104,6 @@ DEPRECATED_SHOW_ERROR_TRACEBACKS_OPTION_WARNING = (
|
|
|
98
104
|
"Warning: Option `--show-errors-tracebacks` is deprecated and will be removed in Schemathesis 4.0. "
|
|
99
105
|
"Use `--show-trace` instead"
|
|
100
106
|
)
|
|
101
|
-
DEPRECATED_CONTRIB_UNIQUE_DATA_OPTION_WARNING = (
|
|
102
|
-
"The `--contrib-unique-data` CLI option and the corresponding `schemathesis.contrib.unique_data` hook "
|
|
103
|
-
"are **DEPRECATED**. The concept of this feature does not fit the core principles of Hypothesis where "
|
|
104
|
-
"strategies are configurable on a per-example basis but this feature implies uniqueness across examples. "
|
|
105
|
-
"This leads to cryptic error messages about external state and flaky test runs, "
|
|
106
|
-
"therefore it will be removed in Schemathesis 4.0"
|
|
107
|
-
)
|
|
108
107
|
CASSETTES_PATH_INVALID_USAGE_MESSAGE = "Can't use `--store-network-log` and `--cassette-path` simultaneously"
|
|
109
108
|
COLOR_OPTIONS_INVALID_USAGE_MESSAGE = "Can't use `--no-color` and `--force-color` simultaneously"
|
|
110
109
|
PHASES_INVALID_USAGE_MESSAGE = "Can't use `--hypothesis-phases` and `--hypothesis-no-phases` simultaneously"
|
|
@@ -114,14 +113,14 @@ def reset_checks() -> None:
|
|
|
114
113
|
"""Get checks list to their default state."""
|
|
115
114
|
# Useful in tests
|
|
116
115
|
checks_module.ALL_CHECKS = checks_module.DEFAULT_CHECKS + checks_module.OPTIONAL_CHECKS
|
|
117
|
-
CHECKS_TYPE.choices = _get_callable_names(checks_module.ALL_CHECKS)
|
|
116
|
+
CHECKS_TYPE.choices = (*_get_callable_names(checks_module.ALL_CHECKS), "all")
|
|
118
117
|
|
|
119
118
|
|
|
120
119
|
def reset_targets() -> None:
|
|
121
120
|
"""Get targets list to their default state."""
|
|
122
121
|
# Useful in tests
|
|
123
122
|
targets_module.ALL_TARGETS = targets_module.DEFAULT_TARGETS + targets_module.OPTIONAL_TARGETS
|
|
124
|
-
TARGETS_TYPE.choices = _get_callable_names(targets_module.ALL_TARGETS)
|
|
123
|
+
TARGETS_TYPE.choices = (*_get_callable_names(targets_module.ALL_TARGETS), "all")
|
|
125
124
|
|
|
126
125
|
|
|
127
126
|
@click.group(context_settings=CONTEXT_SETTINGS)
|
|
@@ -282,7 +281,7 @@ REPORT_TO_SERVICE = ReportToService()
|
|
|
282
281
|
"workers_num",
|
|
283
282
|
help="Number of concurrent workers for testing. Auto-adjusts if 'auto' is specified",
|
|
284
283
|
type=CustomHelpMessageChoice(
|
|
285
|
-
["auto"
|
|
284
|
+
["auto", *list(map(str, range(MIN_WORKERS, MAX_WORKERS + 1)))],
|
|
286
285
|
choices_repr=f"[auto, {MIN_WORKERS}-{MAX_WORKERS}]",
|
|
287
286
|
),
|
|
288
287
|
default=str(DEFAULT_WORKERS),
|
|
@@ -318,7 +317,7 @@ REPORT_TO_SERVICE = ReportToService()
|
|
|
318
317
|
"--fixups",
|
|
319
318
|
help="Apply compatibility adjustments",
|
|
320
319
|
multiple=True,
|
|
321
|
-
type=click.Choice(
|
|
320
|
+
type=click.Choice([*ALL_FIXUPS, "all"]),
|
|
322
321
|
metavar="",
|
|
323
322
|
)
|
|
324
323
|
@group("API validation options")
|
|
@@ -433,7 +432,7 @@ REPORT_TO_SERVICE = ReportToService()
|
|
|
433
432
|
"-A",
|
|
434
433
|
type=click.Choice(["basic", "digest"], case_sensitive=False),
|
|
435
434
|
default="basic",
|
|
436
|
-
help="Specify the authentication method",
|
|
435
|
+
help="Specify the authentication method. For custom authentication methods, see our Authentication documentation: https://schemathesis.readthedocs.io/en/stable/auth.html#custom-auth",
|
|
437
436
|
show_default=True,
|
|
438
437
|
metavar="",
|
|
439
438
|
)
|
|
@@ -943,9 +942,6 @@ def run(
|
|
|
943
942
|
entry for health_check in hypothesis_suppress_health_check for entry in health_check.as_hypothesis()
|
|
944
943
|
]
|
|
945
944
|
|
|
946
|
-
if contrib_unique_data:
|
|
947
|
-
click.secho(DEPRECATED_CONTRIB_UNIQUE_DATA_OPTION_WARNING, fg="yellow")
|
|
948
|
-
|
|
949
945
|
if show_errors_tracebacks:
|
|
950
946
|
click.secho(DEPRECATED_SHOW_ERROR_TRACEBACKS_OPTION_WARNING, fg="yellow")
|
|
951
947
|
show_trace = show_errors_tracebacks
|
|
@@ -1154,8 +1150,6 @@ def run(
|
|
|
1154
1150
|
else:
|
|
1155
1151
|
_fixups.install(fixups)
|
|
1156
1152
|
|
|
1157
|
-
if contrib_unique_data:
|
|
1158
|
-
contrib.unique_data.install()
|
|
1159
1153
|
if contrib_openapi_formats_uuid:
|
|
1160
1154
|
contrib.openapi.formats.uuid.install()
|
|
1161
1155
|
if contrib_openapi_fill_missing_examples:
|
|
@@ -1191,6 +1185,7 @@ def run(
|
|
|
1191
1185
|
seed=hypothesis_seed,
|
|
1192
1186
|
exit_first=exit_first,
|
|
1193
1187
|
max_failures=max_failures,
|
|
1188
|
+
unique_data=contrib_unique_data,
|
|
1194
1189
|
dry_run=dry_run,
|
|
1195
1190
|
store_interactions=cassette_path is not None,
|
|
1196
1191
|
checks=selected_checks,
|
|
@@ -1315,6 +1310,7 @@ def into_event_stream(
|
|
|
1315
1310
|
exit_first: bool,
|
|
1316
1311
|
max_failures: int | None,
|
|
1317
1312
|
rate_limit: str | None,
|
|
1313
|
+
unique_data: bool,
|
|
1318
1314
|
dry_run: bool,
|
|
1319
1315
|
store_interactions: bool,
|
|
1320
1316
|
stateful: Stateful | None,
|
|
@@ -1358,6 +1354,7 @@ def into_event_stream(
|
|
|
1358
1354
|
exit_first=exit_first,
|
|
1359
1355
|
max_failures=max_failures,
|
|
1360
1356
|
started_at=started_at,
|
|
1357
|
+
unique_data=unique_data,
|
|
1361
1358
|
dry_run=dry_run,
|
|
1362
1359
|
store_interactions=store_interactions,
|
|
1363
1360
|
checks=checks,
|
|
@@ -2000,7 +1997,7 @@ def decide_color_output(ctx: click.Context, no_color: bool, force_color: bool) -
|
|
|
2000
1997
|
ctx.color = False
|
|
2001
1998
|
|
|
2002
1999
|
|
|
2003
|
-
def add_option(*args: Any, cls:
|
|
2000
|
+
def add_option(*args: Any, cls: type = click.Option, **kwargs: Any) -> None:
|
|
2004
2001
|
"""Add a new CLI option to `st run`."""
|
|
2005
2002
|
run.params.append(cls(args, **kwargs))
|
|
2006
2003
|
|
|
@@ -2024,10 +2021,10 @@ def add_group(name: str, *, index: int | None = None) -> Group:
|
|
|
2024
2021
|
return Group(name)
|
|
2025
2022
|
|
|
2026
2023
|
|
|
2027
|
-
def handler() -> Callable[[
|
|
2024
|
+
def handler() -> Callable[[type], None]:
|
|
2028
2025
|
"""Register a new CLI event handler."""
|
|
2029
2026
|
|
|
2030
|
-
def _wrapper(cls:
|
|
2027
|
+
def _wrapper(cls: type) -> None:
|
|
2031
2028
|
CUSTOM_HANDLERS.append(cls)
|
|
2032
2029
|
|
|
2033
2030
|
return _wrapper
|
schemathesis/cli/callbacks.py
CHANGED
|
@@ -2,16 +2,16 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import codecs
|
|
4
4
|
import enum
|
|
5
|
+
import operator
|
|
5
6
|
import os
|
|
6
7
|
import re
|
|
7
8
|
import traceback
|
|
8
9
|
from contextlib import contextmanager
|
|
9
|
-
from functools import partial
|
|
10
|
+
from functools import partial, reduce
|
|
10
11
|
from typing import TYPE_CHECKING, Callable, Generator
|
|
11
12
|
from urllib.parse import urlparse
|
|
12
13
|
|
|
13
14
|
import click
|
|
14
|
-
from click.types import LazyFile # type: ignore
|
|
15
15
|
|
|
16
16
|
from .. import exceptions, experimental, throttling
|
|
17
17
|
from ..code_samples import CodeSampleStyle
|
|
@@ -24,12 +24,14 @@ from ..loaders import load_app
|
|
|
24
24
|
from ..service.hosts import get_temporary_hosts_file
|
|
25
25
|
from ..stateful import Stateful
|
|
26
26
|
from ..transports.headers import has_invalid_characters, is_latin_1_encodable
|
|
27
|
-
from ..types import PathLike
|
|
28
27
|
from .cassettes import CassetteFormat
|
|
29
28
|
from .constants import DEFAULT_WORKERS
|
|
30
29
|
|
|
31
30
|
if TYPE_CHECKING:
|
|
32
31
|
import hypothesis
|
|
32
|
+
from click.types import LazyFile # type: ignore[attr-defined]
|
|
33
|
+
|
|
34
|
+
from ..types import PathLike
|
|
33
35
|
|
|
34
36
|
INVALID_DERANDOMIZE_MESSAGE = (
|
|
35
37
|
"`--hypothesis-derandomize` implies no database, so passing `--hypothesis-database` too is invalid."
|
|
@@ -339,7 +341,7 @@ def convert_experimental(
|
|
|
339
341
|
|
|
340
342
|
|
|
341
343
|
def convert_checks(ctx: click.core.Context, param: click.core.Parameter, value: tuple[list[str]]) -> list[str]:
|
|
342
|
-
return
|
|
344
|
+
return reduce(operator.iadd, value, [])
|
|
343
345
|
|
|
344
346
|
|
|
345
347
|
def convert_code_sample_style(ctx: click.core.Context, param: click.core.Parameter, value: str) -> CodeSampleStyle:
|
schemathesis/cli/cassettes.py
CHANGED
|
@@ -16,7 +16,6 @@ import harfile
|
|
|
16
16
|
|
|
17
17
|
from ..constants import SCHEMATHESIS_VERSION
|
|
18
18
|
from ..runner import events
|
|
19
|
-
from ..types import RequestCert
|
|
20
19
|
from .handlers import EventHandler
|
|
21
20
|
|
|
22
21
|
if TYPE_CHECKING:
|
|
@@ -25,6 +24,7 @@ if TYPE_CHECKING:
|
|
|
25
24
|
|
|
26
25
|
from ..models import Request, Response
|
|
27
26
|
from ..runner.serialization import SerializedCheck, SerializedInteraction
|
|
27
|
+
from ..types import RequestCert
|
|
28
28
|
from .context import ExecutionContext
|
|
29
29
|
|
|
30
30
|
# Wait until the worker terminates
|
|
@@ -163,13 +163,18 @@ def vcr_writer(file_handle: click.utils.LazyFile, preserve_exact_body_bytes: boo
|
|
|
163
163
|
return "\n".join(f' "{name}":\n{format_header_values(values)}' for name, values in headers.items())
|
|
164
164
|
|
|
165
165
|
def format_check_message(message: str | None) -> str:
|
|
166
|
-
return "~" if message is None else f"{
|
|
166
|
+
return "~" if message is None else f"{message!r}"
|
|
167
167
|
|
|
168
168
|
def format_checks(checks: list[SerializedCheck]) -> str:
|
|
169
|
-
|
|
169
|
+
if not checks:
|
|
170
|
+
return " checks: []"
|
|
171
|
+
items = "\n".join(
|
|
170
172
|
f" - name: '{check.name}'\n status: '{check.value.name.upper()}'\n message: {format_check_message(check.message)}"
|
|
171
173
|
for check in checks
|
|
172
174
|
)
|
|
175
|
+
return f"""
|
|
176
|
+
checks:
|
|
177
|
+
{items}"""
|
|
173
178
|
|
|
174
179
|
if preserve_exact_body_bytes:
|
|
175
180
|
|
|
@@ -235,9 +240,8 @@ http_interactions:"""
|
|
|
235
240
|
correlation_id: '{item.correlation_id}'
|
|
236
241
|
data_generation_method: '{interaction.data_generation_method.value}'
|
|
237
242
|
phase: {phase}
|
|
238
|
-
elapsed: '{interaction.response.elapsed}'
|
|
243
|
+
elapsed: '{interaction.response.elapsed if interaction.response else 0}'
|
|
239
244
|
recorded_at: '{interaction.recorded_at}'
|
|
240
|
-
checks:
|
|
241
245
|
{format_checks(interaction.checks)}
|
|
242
246
|
request:
|
|
243
247
|
uri: '{interaction.request.uri}'
|
|
@@ -246,8 +250,9 @@ http_interactions:"""
|
|
|
246
250
|
{format_headers(interaction.request.headers)}"""
|
|
247
251
|
)
|
|
248
252
|
format_request_body(stream, interaction.request)
|
|
249
|
-
|
|
250
|
-
|
|
253
|
+
if interaction.response is not None:
|
|
254
|
+
stream.write(
|
|
255
|
+
f"""
|
|
251
256
|
response:
|
|
252
257
|
status:
|
|
253
258
|
code: '{interaction.response.status_code}'
|
|
@@ -255,12 +260,16 @@ http_interactions:"""
|
|
|
255
260
|
headers:
|
|
256
261
|
{format_headers(interaction.response.headers)}
|
|
257
262
|
"""
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
263
|
+
)
|
|
264
|
+
format_response_body(stream, interaction.response)
|
|
265
|
+
stream.write(
|
|
266
|
+
f"""
|
|
262
267
|
http_version: '{interaction.response.http_version}'"""
|
|
263
|
-
|
|
268
|
+
)
|
|
269
|
+
else:
|
|
270
|
+
stream.write("""
|
|
271
|
+
response: null
|
|
272
|
+
""")
|
|
264
273
|
current_id += 1
|
|
265
274
|
else:
|
|
266
275
|
break
|
|
@@ -300,11 +309,11 @@ def write_double_quoted(stream: IO, text: str) -> None:
|
|
|
300
309
|
if ch in Emitter.ESCAPE_REPLACEMENTS:
|
|
301
310
|
data = "\\" + Emitter.ESCAPE_REPLACEMENTS[ch]
|
|
302
311
|
elif ch <= "\xff":
|
|
303
|
-
data = "\\x
|
|
312
|
+
data = f"\\x{ord(ch):02X}"
|
|
304
313
|
elif ch <= "\uffff":
|
|
305
|
-
data = "\\u
|
|
314
|
+
data = f"\\u{ord(ch):04X}"
|
|
306
315
|
else:
|
|
307
|
-
data = "\\U
|
|
316
|
+
data = f"\\U{ord(ch):08X}"
|
|
308
317
|
stream.write(data)
|
|
309
318
|
start = end + 1
|
|
310
319
|
end += 1
|
|
@@ -326,25 +335,45 @@ def har_writer(file_handle: click.utils.LazyFile, preserve_exact_body_bytes: boo
|
|
|
326
335
|
item = queue.get()
|
|
327
336
|
if isinstance(item, Process):
|
|
328
337
|
for interaction in item.interactions:
|
|
329
|
-
time = round(interaction.response.elapsed * 1000, 2)
|
|
330
|
-
content_type = interaction.response.headers.get("Content-Type", [""])[0]
|
|
331
|
-
content = harfile.Content(
|
|
332
|
-
size=interaction.response.body_size or 0,
|
|
333
|
-
mimeType=content_type,
|
|
334
|
-
text=get_body(interaction.response.body) if interaction.response.body is not None else None,
|
|
335
|
-
encoding="base64"
|
|
336
|
-
if interaction.response.body is not None and preserve_exact_body_bytes
|
|
337
|
-
else None,
|
|
338
|
-
)
|
|
339
|
-
http_version = f"HTTP/{interaction.response.http_version}"
|
|
340
338
|
query_params = urlparse(interaction.request.uri).query
|
|
341
339
|
if interaction.request.body is not None:
|
|
342
340
|
post_data = harfile.PostData(
|
|
343
|
-
mimeType=
|
|
341
|
+
mimeType=interaction.request.headers.get("Content-Type", [""])[0],
|
|
344
342
|
text=get_body(interaction.request.body),
|
|
345
343
|
)
|
|
346
344
|
else:
|
|
347
345
|
post_data = None
|
|
346
|
+
if interaction.response is not None:
|
|
347
|
+
content_type = interaction.response.headers.get("Content-Type", [""])[0]
|
|
348
|
+
content = harfile.Content(
|
|
349
|
+
size=interaction.response.body_size or 0,
|
|
350
|
+
mimeType=content_type,
|
|
351
|
+
text=get_body(interaction.response.body) if interaction.response.body is not None else None,
|
|
352
|
+
encoding="base64"
|
|
353
|
+
if interaction.response.body is not None and preserve_exact_body_bytes
|
|
354
|
+
else None,
|
|
355
|
+
)
|
|
356
|
+
http_version = f"HTTP/{interaction.response.http_version}"
|
|
357
|
+
response = harfile.Response(
|
|
358
|
+
status=interaction.response.status_code,
|
|
359
|
+
httpVersion=http_version,
|
|
360
|
+
statusText=interaction.response.message,
|
|
361
|
+
headers=[
|
|
362
|
+
harfile.Record(name=name, value=values[0])
|
|
363
|
+
for name, values in interaction.response.headers.items()
|
|
364
|
+
],
|
|
365
|
+
cookies=_extract_cookies(interaction.response.headers.get("Set-Cookie", [])),
|
|
366
|
+
content=content,
|
|
367
|
+
headersSize=_headers_size(interaction.response.headers),
|
|
368
|
+
bodySize=interaction.response.body_size or 0,
|
|
369
|
+
redirectURL=interaction.response.headers.get("Location", [""])[0],
|
|
370
|
+
)
|
|
371
|
+
time = round(interaction.response.elapsed * 1000, 2)
|
|
372
|
+
else:
|
|
373
|
+
response = HARFILE_NO_RESPONSE
|
|
374
|
+
time = 0
|
|
375
|
+
http_version = ""
|
|
376
|
+
|
|
348
377
|
har.add_entry(
|
|
349
378
|
startedDateTime=interaction.recorded_at,
|
|
350
379
|
time=time,
|
|
@@ -365,26 +394,23 @@ def har_writer(file_handle: click.utils.LazyFile, preserve_exact_body_bytes: boo
|
|
|
365
394
|
bodySize=interaction.request.body_size or 0,
|
|
366
395
|
postData=post_data,
|
|
367
396
|
),
|
|
368
|
-
response=
|
|
369
|
-
status=interaction.response.status_code,
|
|
370
|
-
httpVersion=http_version,
|
|
371
|
-
statusText=interaction.response.message,
|
|
372
|
-
headers=[
|
|
373
|
-
harfile.Record(name=name, value=values[0])
|
|
374
|
-
for name, values in interaction.response.headers.items()
|
|
375
|
-
],
|
|
376
|
-
cookies=_extract_cookies(interaction.response.headers.get("Set-Cookie", [])),
|
|
377
|
-
content=content,
|
|
378
|
-
headersSize=_headers_size(interaction.response.headers),
|
|
379
|
-
bodySize=interaction.response.body_size or 0,
|
|
380
|
-
redirectURL=interaction.response.headers.get("Location", [""])[0],
|
|
381
|
-
),
|
|
397
|
+
response=response,
|
|
382
398
|
timings=harfile.Timings(send=0, wait=0, receive=time, blocked=0, dns=0, connect=0, ssl=0),
|
|
383
399
|
)
|
|
384
400
|
elif isinstance(item, Finalize):
|
|
385
401
|
break
|
|
386
402
|
|
|
387
403
|
|
|
404
|
+
HARFILE_NO_RESPONSE = harfile.Response(
|
|
405
|
+
status=0,
|
|
406
|
+
httpVersion="",
|
|
407
|
+
statusText="",
|
|
408
|
+
headers=[],
|
|
409
|
+
cookies=[],
|
|
410
|
+
content=harfile.Content(),
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
|
|
388
414
|
def _headers_size(headers: dict[str, list[str]]) -> int:
|
|
389
415
|
size = 0
|
|
390
416
|
for name, values in headers.items():
|
schemathesis/cli/context.py
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
3
|
import shutil
|
|
5
4
|
from dataclasses import dataclass, field
|
|
6
|
-
from queue import Queue
|
|
7
5
|
from typing import TYPE_CHECKING, Generator
|
|
8
6
|
|
|
9
7
|
from ..code_samples import CodeSampleStyle
|
|
10
8
|
from ..internal.deprecation import deprecated_property
|
|
11
9
|
from ..internal.output import OutputConfig
|
|
12
|
-
from ..internal.result import Result
|
|
13
|
-
from ..runner.probes import ProbeRun
|
|
14
|
-
from ..runner.serialization import SerializedTestResult
|
|
15
|
-
from ..service.models import AnalysisResult
|
|
16
10
|
|
|
17
11
|
if TYPE_CHECKING:
|
|
12
|
+
import os
|
|
13
|
+
from queue import Queue
|
|
14
|
+
|
|
18
15
|
import hypothesis
|
|
19
16
|
|
|
17
|
+
from ..internal.result import Result
|
|
18
|
+
from ..runner.probes import ProbeRun
|
|
19
|
+
from ..runner.serialization import SerializedTestResult
|
|
20
|
+
from ..service.models import AnalysisResult
|
|
20
21
|
from ..stateful.sink import StateMachineSink
|
|
21
22
|
|
|
22
23
|
|
schemathesis/cli/junitxml.py
CHANGED
|
@@ -11,13 +11,13 @@ from ..exceptions import RuntimeErrorType
|
|
|
11
11
|
from ..internal.output import prepare_response_payload
|
|
12
12
|
from ..models import Status
|
|
13
13
|
from ..runner import events
|
|
14
|
-
from ..runner.serialization import SerializedCheck, SerializedError
|
|
15
14
|
from .handlers import EventHandler
|
|
16
15
|
from .reporting import TEST_CASE_ID_TITLE, get_runtime_error_suggestion, group_by_case, split_traceback
|
|
17
16
|
|
|
18
17
|
if TYPE_CHECKING:
|
|
19
18
|
from click.utils import LazyFile
|
|
20
19
|
|
|
20
|
+
from ..runner.serialization import SerializedCheck, SerializedError
|
|
21
21
|
from .context import ExecutionContext
|
|
22
22
|
|
|
23
23
|
|
schemathesis/cli/options.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from typing import Any, NoReturn
|
|
3
|
+
from typing import TYPE_CHECKING, Any, NoReturn
|
|
5
4
|
|
|
6
5
|
import click
|
|
7
6
|
|
|
8
7
|
from ..constants import NOT_SET
|
|
9
|
-
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from enum import Enum
|
|
11
|
+
|
|
12
|
+
from ..types import NotSet
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
class CustomHelpMessageChoice(click.Choice):
|
|
@@ -65,4 +68,4 @@ class OptionalInt(click.types.IntRange):
|
|
|
65
68
|
int(value)
|
|
66
69
|
return super().convert(value, param, ctx)
|
|
67
70
|
except ValueError:
|
|
68
|
-
self.fail("
|
|
71
|
+
self.fail(f"{value} is not a valid integer or None.", param, ctx)
|