schemathesis 3.35.4__py3-none-any.whl → 3.35.5__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/cli/__init__.py +19 -13
- 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/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 +5 -5
- schemathesis/graphql.py +0 -1
- schemathesis/hooks.py +3 -3
- schemathesis/lazy.py +10 -7
- schemathesis/loaders.py +3 -3
- schemathesis/models.py +39 -15
- schemathesis/runner/__init__.py +5 -5
- schemathesis/runner/events.py +1 -1
- schemathesis/runner/impl/context.py +58 -0
- schemathesis/runner/impl/core.py +54 -61
- schemathesis/runner/impl/solo.py +17 -20
- schemathesis/runner/impl/threadpool.py +65 -71
- schemathesis/runner/serialization.py +4 -3
- schemathesis/sanitization.py +2 -1
- schemathesis/schemas.py +18 -20
- 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 +4 -3
- schemathesis/specs/graphql/schemas.py +4 -3
- 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 +5 -4
- 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 +9 -10
- 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 +1 -1
- schemathesis/stateful/context.py +3 -3
- schemathesis/stateful/events.py +3 -3
- schemathesis/stateful/runner.py +5 -4
- schemathesis/stateful/sink.py +1 -1
- schemathesis/stateful/state_machine.py +5 -5
- schemathesis/stateful/statistic.py +3 -1
- schemathesis/stateful/validation.py +1 -1
- 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.35.5.dist-info}/METADATA +1 -1
- schemathesis-3.35.5.dist-info/RECORD +156 -0
- schemathesis-3.35.4.dist-info/RECORD +0 -154
- {schemathesis-3.35.4.dist-info → schemathesis-3.35.5.dist-info}/WHEEL +0 -0
- {schemathesis-3.35.4.dist-info → schemathesis-3.35.5.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.35.4.dist-info → schemathesis-3.35.5.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/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)
|
|
@@ -114,14 +120,14 @@ def reset_checks() -> None:
|
|
|
114
120
|
"""Get checks list to their default state."""
|
|
115
121
|
# Useful in tests
|
|
116
122
|
checks_module.ALL_CHECKS = checks_module.DEFAULT_CHECKS + checks_module.OPTIONAL_CHECKS
|
|
117
|
-
CHECKS_TYPE.choices = _get_callable_names(checks_module.ALL_CHECKS)
|
|
123
|
+
CHECKS_TYPE.choices = (*_get_callable_names(checks_module.ALL_CHECKS), "all")
|
|
118
124
|
|
|
119
125
|
|
|
120
126
|
def reset_targets() -> None:
|
|
121
127
|
"""Get targets list to their default state."""
|
|
122
128
|
# Useful in tests
|
|
123
129
|
targets_module.ALL_TARGETS = targets_module.DEFAULT_TARGETS + targets_module.OPTIONAL_TARGETS
|
|
124
|
-
TARGETS_TYPE.choices = _get_callable_names(targets_module.ALL_TARGETS)
|
|
130
|
+
TARGETS_TYPE.choices = (*_get_callable_names(targets_module.ALL_TARGETS), "all")
|
|
125
131
|
|
|
126
132
|
|
|
127
133
|
@click.group(context_settings=CONTEXT_SETTINGS)
|
|
@@ -282,7 +288,7 @@ REPORT_TO_SERVICE = ReportToService()
|
|
|
282
288
|
"workers_num",
|
|
283
289
|
help="Number of concurrent workers for testing. Auto-adjusts if 'auto' is specified",
|
|
284
290
|
type=CustomHelpMessageChoice(
|
|
285
|
-
["auto"
|
|
291
|
+
["auto", *list(map(str, range(MIN_WORKERS, MAX_WORKERS + 1)))],
|
|
286
292
|
choices_repr=f"[auto, {MIN_WORKERS}-{MAX_WORKERS}]",
|
|
287
293
|
),
|
|
288
294
|
default=str(DEFAULT_WORKERS),
|
|
@@ -318,7 +324,7 @@ REPORT_TO_SERVICE = ReportToService()
|
|
|
318
324
|
"--fixups",
|
|
319
325
|
help="Apply compatibility adjustments",
|
|
320
326
|
multiple=True,
|
|
321
|
-
type=click.Choice(
|
|
327
|
+
type=click.Choice([*ALL_FIXUPS, "all"]),
|
|
322
328
|
metavar="",
|
|
323
329
|
)
|
|
324
330
|
@group("API validation options")
|
|
@@ -2000,7 +2006,7 @@ def decide_color_output(ctx: click.Context, no_color: bool, force_color: bool) -
|
|
|
2000
2006
|
ctx.color = False
|
|
2001
2007
|
|
|
2002
2008
|
|
|
2003
|
-
def add_option(*args: Any, cls:
|
|
2009
|
+
def add_option(*args: Any, cls: type = click.Option, **kwargs: Any) -> None:
|
|
2004
2010
|
"""Add a new CLI option to `st run`."""
|
|
2005
2011
|
run.params.append(cls(args, **kwargs))
|
|
2006
2012
|
|
|
@@ -2024,10 +2030,10 @@ def add_group(name: str, *, index: int | None = None) -> Group:
|
|
|
2024
2030
|
return Group(name)
|
|
2025
2031
|
|
|
2026
2032
|
|
|
2027
|
-
def handler() -> Callable[[
|
|
2033
|
+
def handler() -> Callable[[type], None]:
|
|
2028
2034
|
"""Register a new CLI event handler."""
|
|
2029
2035
|
|
|
2030
|
-
def _wrapper(cls:
|
|
2036
|
+
def _wrapper(cls: type) -> None:
|
|
2031
2037
|
CUSTOM_HANDLERS.append(cls)
|
|
2032
2038
|
|
|
2033
2039
|
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)
|
|
@@ -6,7 +6,6 @@ import shutil
|
|
|
6
6
|
import textwrap
|
|
7
7
|
import time
|
|
8
8
|
from importlib import metadata
|
|
9
|
-
from queue import Queue
|
|
10
9
|
from types import GeneratorType
|
|
11
10
|
from typing import TYPE_CHECKING, Any, Generator, Literal, cast
|
|
12
11
|
|
|
@@ -44,6 +43,8 @@ from ..handlers import EventHandler
|
|
|
44
43
|
from ..reporting import TEST_CASE_ID_TITLE, get_runtime_error_suggestion, group_by_case, split_traceback
|
|
45
44
|
|
|
46
45
|
if TYPE_CHECKING:
|
|
46
|
+
from queue import Queue
|
|
47
|
+
|
|
47
48
|
import requests
|
|
48
49
|
|
|
49
50
|
SPINNER_REPETITION_NUMBER = 10
|
|
@@ -372,7 +373,7 @@ def display_analysis(context: ExecutionContext) -> None:
|
|
|
372
373
|
click.echo()
|
|
373
374
|
if isinstance(analysis, AnalysisSuccess):
|
|
374
375
|
click.secho(analysis.message, bold=True)
|
|
375
|
-
click.echo("\nAnalysis took: {:.2f}ms"
|
|
376
|
+
click.echo(f"\nAnalysis took: {analysis.elapsed:.2f}ms")
|
|
376
377
|
if analysis.extensions:
|
|
377
378
|
known = []
|
|
378
379
|
failed = []
|
|
@@ -417,8 +418,8 @@ def display_analysis(context: ExecutionContext) -> None:
|
|
|
417
418
|
click.secho("Error\n", fg="red", bold=True)
|
|
418
419
|
_display_service_network_error(response)
|
|
419
420
|
click.echo()
|
|
420
|
-
return
|
|
421
|
-
|
|
421
|
+
return
|
|
422
|
+
if isinstance(exception, requests.RequestException):
|
|
422
423
|
message, extras = extract_requests_exception_details(exception)
|
|
423
424
|
suggestion = "Please check your network connection and try again."
|
|
424
425
|
title = "Network Error"
|
|
@@ -649,7 +650,6 @@ def wait_for_report_handler(queue: Queue, title: str, timeout: float = service.W
|
|
|
649
650
|
|
|
650
651
|
def create_spinner(repetitions: int) -> Generator[str, None, None]:
|
|
651
652
|
"""A simple spinner that yields its individual characters."""
|
|
652
|
-
assert repetitions > 0, "The number of repetitions should be greater than zero"
|
|
653
653
|
while True:
|
|
654
654
|
for ch in "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏":
|
|
655
655
|
# Skip branch coverage, as it is not possible because of the assertion above
|
schemathesis/cli/reporting.py
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from itertools import groupby
|
|
4
|
-
from typing import Callable, Generator, Iterator
|
|
4
|
+
from typing import TYPE_CHECKING, Callable, Generator, Iterator
|
|
5
5
|
|
|
6
6
|
import click
|
|
7
7
|
|
|
8
|
-
from ..code_samples import CodeSampleStyle
|
|
9
8
|
from ..exceptions import RuntimeErrorType
|
|
10
9
|
from ..runner.serialization import SerializedCheck, deduplicate_failures
|
|
11
10
|
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ..code_samples import CodeSampleStyle
|
|
13
|
+
|
|
12
14
|
TEST_CASE_ID_TITLE = "Test Case ID"
|
|
13
15
|
|
|
14
16
|
|
schemathesis/code_samples.py
CHANGED
|
@@ -6,11 +6,12 @@ from shlex import quote
|
|
|
6
6
|
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
8
|
from .constants import SCHEMATHESIS_TEST_CASE_HEADER
|
|
9
|
-
from .types import Headers
|
|
10
9
|
|
|
11
10
|
if TYPE_CHECKING:
|
|
12
11
|
from requests.structures import CaseInsensitiveDict
|
|
13
12
|
|
|
13
|
+
from .types import Headers
|
|
14
|
+
|
|
14
15
|
|
|
15
16
|
@lru_cache
|
|
16
17
|
def get_excluded_headers() -> CaseInsensitiveDict:
|
|
@@ -120,9 +121,9 @@ def _generate_requests(
|
|
|
120
121
|
url = _escape_single_quotes(url)
|
|
121
122
|
command = f"requests.{method.lower()}('{url}'"
|
|
122
123
|
if body:
|
|
123
|
-
command += f", data={
|
|
124
|
+
command += f", data={body!r}"
|
|
124
125
|
if headers:
|
|
125
|
-
command += f", headers={
|
|
126
|
+
command += f", headers={headers!r}"
|
|
126
127
|
if not verify:
|
|
127
128
|
command += ", verify=False"
|
|
128
129
|
command += ")"
|
schemathesis/exceptions.py
CHANGED
|
@@ -5,21 +5,22 @@ import re
|
|
|
5
5
|
import traceback
|
|
6
6
|
from dataclasses import dataclass, field
|
|
7
7
|
from hashlib import sha1
|
|
8
|
-
from json import JSONDecodeError
|
|
9
|
-
from types import TracebackType
|
|
10
8
|
from typing import TYPE_CHECKING, Any, Callable, Generator, NoReturn
|
|
11
9
|
|
|
12
10
|
from .constants import SERIALIZERS_SUGGESTION_MESSAGE
|
|
13
|
-
from .failures import FailureContext
|
|
14
11
|
from .internal.output import truncate_json
|
|
15
12
|
|
|
16
13
|
if TYPE_CHECKING:
|
|
14
|
+
from json import JSONDecodeError
|
|
15
|
+
from types import TracebackType
|
|
16
|
+
|
|
17
17
|
import hypothesis.errors
|
|
18
18
|
from graphql.error import GraphQLFormattedError
|
|
19
19
|
from jsonschema import RefResolutionError, ValidationError
|
|
20
20
|
from jsonschema import SchemaError as JsonSchemaError
|
|
21
21
|
from requests import RequestException
|
|
22
22
|
|
|
23
|
+
from .failures import FailureContext
|
|
23
24
|
from .transports.responses import GenericResponse
|
|
24
25
|
|
|
25
26
|
|