schemathesis 3.29.2__py3-none-any.whl → 3.30.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/__init__.py +3 -3
- schemathesis/_compat.py +2 -2
- schemathesis/_dependency_versions.py +1 -3
- schemathesis/_hypothesis.py +6 -0
- schemathesis/_lazy_import.py +1 -0
- schemathesis/_override.py +1 -0
- schemathesis/_rate_limiter.py +2 -1
- schemathesis/_xml.py +1 -0
- schemathesis/auths.py +4 -2
- schemathesis/checks.py +8 -5
- schemathesis/cli/__init__.py +28 -1
- schemathesis/cli/callbacks.py +3 -4
- schemathesis/cli/cassettes.py +6 -4
- schemathesis/cli/constants.py +2 -0
- schemathesis/cli/context.py +5 -0
- schemathesis/cli/debug.py +2 -1
- schemathesis/cli/handlers.py +1 -1
- schemathesis/cli/junitxml.py +5 -4
- schemathesis/cli/options.py +1 -0
- schemathesis/cli/output/default.py +56 -24
- schemathesis/cli/output/short.py +21 -10
- schemathesis/cli/sanitization.py +1 -0
- schemathesis/code_samples.py +1 -0
- schemathesis/constants.py +1 -0
- schemathesis/contrib/openapi/__init__.py +1 -1
- schemathesis/contrib/openapi/fill_missing_examples.py +2 -0
- schemathesis/contrib/openapi/formats/uuid.py +2 -1
- schemathesis/contrib/unique_data.py +2 -1
- schemathesis/exceptions.py +42 -61
- schemathesis/experimental/__init__.py +14 -0
- schemathesis/extra/_aiohttp.py +1 -0
- schemathesis/extra/_server.py +1 -0
- schemathesis/extra/pytest_plugin.py +13 -24
- schemathesis/failures.py +42 -8
- schemathesis/filters.py +2 -1
- schemathesis/fixups/__init__.py +1 -0
- schemathesis/fixups/fast_api.py +2 -2
- schemathesis/fixups/utf8_bom.py +1 -2
- schemathesis/generation/__init__.py +2 -1
- schemathesis/hooks.py +3 -1
- schemathesis/internal/copy.py +19 -3
- schemathesis/internal/deprecation.py +1 -1
- schemathesis/internal/jsonschema.py +2 -1
- schemathesis/internal/output.py +68 -0
- schemathesis/internal/result.py +1 -1
- schemathesis/internal/transformation.py +1 -0
- schemathesis/lazy.py +11 -2
- schemathesis/loaders.py +4 -2
- schemathesis/models.py +22 -7
- schemathesis/parameters.py +1 -0
- schemathesis/runner/__init__.py +1 -1
- schemathesis/runner/events.py +22 -4
- schemathesis/runner/impl/core.py +69 -33
- schemathesis/runner/impl/solo.py +2 -1
- schemathesis/runner/impl/threadpool.py +4 -0
- schemathesis/runner/probes.py +1 -1
- schemathesis/runner/serialization.py +1 -1
- schemathesis/sanitization.py +2 -0
- schemathesis/schemas.py +7 -4
- schemathesis/service/ci.py +1 -0
- schemathesis/service/client.py +7 -7
- schemathesis/service/events.py +2 -1
- schemathesis/service/extensions.py +5 -5
- schemathesis/service/hosts.py +1 -0
- schemathesis/service/metadata.py +2 -1
- schemathesis/service/models.py +2 -1
- schemathesis/service/report.py +3 -3
- schemathesis/service/serialization.py +62 -23
- schemathesis/service/usage.py +1 -0
- schemathesis/specs/graphql/_cache.py +1 -1
- schemathesis/specs/graphql/loaders.py +17 -1
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +2 -2
- schemathesis/specs/graphql/schemas.py +7 -7
- schemathesis/specs/graphql/validation.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +17 -11
- schemathesis/specs/openapi/checks.py +102 -9
- schemathesis/specs/openapi/converter.py +2 -1
- schemathesis/specs/openapi/definitions.py +2 -1
- schemathesis/specs/openapi/examples.py +7 -9
- schemathesis/specs/openapi/expressions/__init__.py +29 -2
- schemathesis/specs/openapi/expressions/context.py +1 -1
- schemathesis/specs/openapi/expressions/extractors.py +23 -0
- schemathesis/specs/openapi/expressions/lexer.py +19 -18
- schemathesis/specs/openapi/expressions/nodes.py +24 -4
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/filters.py +1 -0
- schemathesis/specs/openapi/links.py +35 -7
- schemathesis/specs/openapi/loaders.py +31 -11
- schemathesis/specs/openapi/negative/__init__.py +2 -1
- schemathesis/specs/openapi/negative/mutations.py +1 -0
- schemathesis/specs/openapi/parameters.py +1 -0
- schemathesis/specs/openapi/schemas.py +28 -39
- schemathesis/specs/openapi/security.py +1 -0
- schemathesis/specs/openapi/serialization.py +1 -0
- schemathesis/specs/openapi/stateful/__init__.py +159 -70
- schemathesis/specs/openapi/stateful/statistic.py +198 -0
- schemathesis/specs/openapi/stateful/types.py +13 -0
- schemathesis/specs/openapi/utils.py +1 -0
- schemathesis/specs/openapi/validation.py +1 -0
- schemathesis/stateful/__init__.py +4 -2
- schemathesis/stateful/config.py +66 -0
- schemathesis/stateful/context.py +103 -0
- schemathesis/stateful/events.py +215 -0
- schemathesis/stateful/runner.py +238 -0
- schemathesis/stateful/sink.py +68 -0
- schemathesis/stateful/state_machine.py +39 -22
- schemathesis/stateful/statistic.py +20 -0
- schemathesis/stateful/validation.py +66 -0
- schemathesis/targets.py +1 -0
- schemathesis/throttling.py +23 -3
- schemathesis/transports/__init__.py +28 -10
- schemathesis/transports/auth.py +1 -0
- schemathesis/transports/content_types.py +1 -1
- schemathesis/transports/headers.py +2 -1
- schemathesis/transports/responses.py +6 -4
- schemathesis/types.py +1 -0
- schemathesis/utils.py +1 -0
- {schemathesis-3.29.2.dist-info → schemathesis-3.30.1.dist-info}/METADATA +3 -3
- schemathesis-3.30.1.dist-info/RECORD +151 -0
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis-3.29.2.dist-info/RECORD +0 -141
- {schemathesis-3.29.2.dist-info → schemathesis-3.30.1.dist-info}/WHEEL +0 -0
- {schemathesis-3.29.2.dist-info → schemathesis-3.30.1.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.29.2.dist-info → schemathesis-3.30.1.dist-info}/licenses/LICENSE +0 -0
schemathesis/__init__.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
from typing import Any
|
|
3
4
|
|
|
4
|
-
from . import auths, checks,
|
|
5
|
+
from . import auths, checks, contrib, experimental, fixups, graphql, hooks, runner, serializers, targets # noqa: E402
|
|
5
6
|
from ._lazy_import import lazy_import
|
|
6
|
-
from .generation import DataGenerationMethod, GenerationConfig, HeaderConfig # noqa: E402
|
|
7
7
|
from .constants import SCHEMATHESIS_VERSION # noqa: E402
|
|
8
|
+
from .generation import DataGenerationMethod, GenerationConfig, HeaderConfig # noqa: E402
|
|
8
9
|
from .models import Case # noqa: E402
|
|
9
10
|
from .specs import openapi # noqa: E402
|
|
10
11
|
|
|
11
|
-
|
|
12
12
|
__version__ = SCHEMATHESIS_VERSION
|
|
13
13
|
|
|
14
14
|
# Default loaders
|
schemathesis/_compat.py
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
"""Compatibility flags based on installed dependency versions."""
|
|
2
2
|
|
|
3
|
-
from packaging import version
|
|
4
|
-
|
|
5
3
|
from importlib import metadata
|
|
6
4
|
|
|
5
|
+
from packaging import version
|
|
7
6
|
|
|
8
7
|
WERKZEUG_VERSION = version.parse(metadata.version("werkzeug"))
|
|
9
8
|
IS_WERKZEUG_ABOVE_3 = WERKZEUG_VERSION >= version.parse("3.0")
|
|
10
9
|
IS_WERKZEUG_BELOW_2_1 = WERKZEUG_VERSION < version.parse("2.1.0")
|
|
11
10
|
|
|
12
11
|
PYTEST_VERSION = version.parse(metadata.version("pytest"))
|
|
13
|
-
IS_PYTEST_ABOVE_54 = PYTEST_VERSION >= version.parse("5.4.0")
|
|
14
12
|
IS_PYTEST_ABOVE_7 = PYTEST_VERSION >= version.parse("7.0.0")
|
|
15
13
|
IS_PYTEST_ABOVE_8 = PYTEST_VERSION >= version.parse("8.0.0")
|
|
16
14
|
|
schemathesis/_hypothesis.py
CHANGED
|
@@ -10,6 +10,7 @@ import hypothesis
|
|
|
10
10
|
from hypothesis import Phase
|
|
11
11
|
from hypothesis import strategies as st
|
|
12
12
|
from hypothesis.errors import HypothesisWarning, Unsatisfiable
|
|
13
|
+
from hypothesis.internal.entropy import deterministic_PRNG
|
|
13
14
|
from hypothesis.internal.reflection import proxies
|
|
14
15
|
from jsonschema.exceptions import SchemaError
|
|
15
16
|
|
|
@@ -23,6 +24,11 @@ from .transports.content_types import parse_content_type
|
|
|
23
24
|
from .transports.headers import has_invalid_characters, is_latin_1_encodable
|
|
24
25
|
from .utils import GivenInput, combine_strategies
|
|
25
26
|
|
|
27
|
+
# Forcefully initializes Hypothesis' global PRNG to avoid races that initilize it
|
|
28
|
+
# if e.g. Schemathesis CLI is used with multiple workers
|
|
29
|
+
with deterministic_PRNG():
|
|
30
|
+
pass
|
|
31
|
+
|
|
26
32
|
|
|
27
33
|
def create_test(
|
|
28
34
|
*,
|
schemathesis/_lazy_import.py
CHANGED
schemathesis/_override.py
CHANGED
schemathesis/_rate_limiter.py
CHANGED
|
@@ -3,4 +3,5 @@ from ._dependency_versions import IS_PYRATE_LIMITER_ABOVE_3
|
|
|
3
3
|
if IS_PYRATE_LIMITER_ABOVE_3:
|
|
4
4
|
from pyrate_limiter import Limiter, Rate, RateItem
|
|
5
5
|
else:
|
|
6
|
-
from pyrate_limiter import Limiter
|
|
6
|
+
from pyrate_limiter import Limiter
|
|
7
|
+
from pyrate_limiter import RequestRate as Rate
|
schemathesis/_xml.py
CHANGED
schemathesis/auths.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Support for custom API authentication mechanisms."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
+
|
|
4
5
|
import inspect
|
|
5
6
|
import threading
|
|
6
7
|
import time
|
|
@@ -11,10 +12,10 @@ from typing import (
|
|
|
11
12
|
Any,
|
|
12
13
|
Callable,
|
|
13
14
|
Generic,
|
|
15
|
+
Protocol,
|
|
14
16
|
TypeVar,
|
|
15
17
|
overload,
|
|
16
18
|
runtime_checkable,
|
|
17
|
-
Protocol,
|
|
18
19
|
)
|
|
19
20
|
|
|
20
21
|
from .exceptions import UsageError
|
|
@@ -22,9 +23,10 @@ from .filters import FilterSet, FilterValue, MatcherFunc, attach_filter_chain
|
|
|
22
23
|
from .types import GenericTest
|
|
23
24
|
|
|
24
25
|
if TYPE_CHECKING:
|
|
25
|
-
from .models import APIOperation, Case
|
|
26
26
|
import requests.auth
|
|
27
27
|
|
|
28
|
+
from .models import APIOperation, Case
|
|
29
|
+
|
|
28
30
|
DEFAULT_REFRESH_INTERVAL = 300
|
|
29
31
|
AUTH_STORAGE_ATTRIBUTE_NAME = "_schemathesis_auth"
|
|
30
32
|
Auth = TypeVar("Auth")
|
schemathesis/checks.py
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import json
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
4
5
|
|
|
5
6
|
from . import failures
|
|
6
|
-
from .exceptions import
|
|
7
|
+
from .exceptions import get_response_parsing_error, get_server_error
|
|
7
8
|
from .specs.openapi.checks import (
|
|
8
9
|
content_type_conformance,
|
|
10
|
+
negative_data_rejection,
|
|
9
11
|
response_headers_conformance,
|
|
10
12
|
response_schema_conformance,
|
|
11
13
|
status_code_conformance,
|
|
12
14
|
)
|
|
13
15
|
|
|
14
16
|
if TYPE_CHECKING:
|
|
15
|
-
from .transports.responses import GenericResponse
|
|
16
17
|
from .models import Case, CheckFunction
|
|
18
|
+
from .transports.responses import GenericResponse
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
def not_a_server_error(response: GenericResponse, case: Case) -> bool | None:
|
|
@@ -24,14 +26,14 @@ def not_a_server_error(response: GenericResponse, case: Case) -> bool | None:
|
|
|
24
26
|
|
|
25
27
|
status_code = response.status_code
|
|
26
28
|
if status_code >= 500:
|
|
27
|
-
exc_class = get_server_error(status_code)
|
|
29
|
+
exc_class = get_server_error(case.operation.verbose_name, status_code)
|
|
28
30
|
raise exc_class(failures.ServerError.title, context=failures.ServerError(status_code=status_code))
|
|
29
31
|
if isinstance(case, GraphQLCase):
|
|
30
32
|
try:
|
|
31
33
|
data = get_json(response)
|
|
32
34
|
validate_graphql_response(data)
|
|
33
35
|
except json.JSONDecodeError as exc:
|
|
34
|
-
exc_class = get_response_parsing_error(exc)
|
|
36
|
+
exc_class = get_response_parsing_error(case.operation.verbose_name, exc)
|
|
35
37
|
context = failures.JSONDecodeErrorContext.from_exception(exc)
|
|
36
38
|
raise exc_class(context.title, context=context) from exc
|
|
37
39
|
return None
|
|
@@ -43,6 +45,7 @@ OPTIONAL_CHECKS = (
|
|
|
43
45
|
content_type_conformance,
|
|
44
46
|
response_headers_conformance,
|
|
45
47
|
response_schema_conformance,
|
|
48
|
+
negative_data_rejection,
|
|
46
49
|
)
|
|
47
50
|
ALL_CHECKS: tuple[CheckFunction, ...] = DEFAULT_CHECKS + OPTIONAL_CHECKS
|
|
48
51
|
|
schemathesis/cli/__init__.py
CHANGED
|
@@ -38,6 +38,7 @@ from ..fixups import ALL_FIXUPS
|
|
|
38
38
|
from ..generation import DEFAULT_DATA_GENERATION_METHODS, DataGenerationMethod
|
|
39
39
|
from ..hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookScope
|
|
40
40
|
from ..internal.datetime import current_datetime
|
|
41
|
+
from ..internal.output import OutputConfig
|
|
41
42
|
from ..internal.validation import file_exists
|
|
42
43
|
from ..loaders import load_app, load_yaml
|
|
43
44
|
from ..models import Case, CheckFunction
|
|
@@ -683,10 +684,25 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
|
|
|
683
684
|
"--experimental",
|
|
684
685
|
"experiments",
|
|
685
686
|
help="Enable experimental support for specific features.",
|
|
686
|
-
type=click.Choice(
|
|
687
|
+
type=click.Choice(
|
|
688
|
+
[
|
|
689
|
+
experimental.OPEN_API_3_1.name,
|
|
690
|
+
experimental.SCHEMA_ANALYSIS.name,
|
|
691
|
+
experimental.STATEFUL_TEST_RUNNER.name,
|
|
692
|
+
experimental.STATEFUL_ONLY.name,
|
|
693
|
+
]
|
|
694
|
+
),
|
|
687
695
|
callback=callbacks.convert_experimental,
|
|
688
696
|
multiple=True,
|
|
689
697
|
)
|
|
698
|
+
@click.option(
|
|
699
|
+
"--output-truncate",
|
|
700
|
+
help="Specifies whether to truncate schemas and responses in error messages.",
|
|
701
|
+
type=str,
|
|
702
|
+
default="true",
|
|
703
|
+
show_default=True,
|
|
704
|
+
callback=callbacks.convert_boolean_string,
|
|
705
|
+
)
|
|
690
706
|
@click.option(
|
|
691
707
|
"--generation-allow-x00",
|
|
692
708
|
help="Determines whether to allow the generation of `\x00` bytes within strings.",
|
|
@@ -776,6 +792,7 @@ def run(
|
|
|
776
792
|
stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT,
|
|
777
793
|
force_schema_version: str | None = None,
|
|
778
794
|
sanitize_output: bool = True,
|
|
795
|
+
output_truncate: bool = True,
|
|
779
796
|
contrib_unique_data: bool = False,
|
|
780
797
|
contrib_openapi_formats_uuid: bool = False,
|
|
781
798
|
contrib_openapi_fill_missing_examples: bool = False,
|
|
@@ -856,6 +873,8 @@ def run(
|
|
|
856
873
|
click.secho(DEPRECATED_CASSETTE_PATH_OPTION_WARNING, fg="yellow")
|
|
857
874
|
cassette_path = store_network_log
|
|
858
875
|
|
|
876
|
+
output_config = OutputConfig(truncate=output_truncate)
|
|
877
|
+
|
|
859
878
|
schemathesis_io_hostname = urlparse(schemathesis_io_url).netloc
|
|
860
879
|
token = schemathesis_io_token or service.hosts.get_token(hostname=schemathesis_io_hostname, hosts_file=hosts_file)
|
|
861
880
|
schema_kind = callbacks.parse_schema_kind(schema, app)
|
|
@@ -975,6 +994,7 @@ def run(
|
|
|
975
994
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
976
995
|
hypothesis_settings=hypothesis_settings,
|
|
977
996
|
generation_config=generation_config,
|
|
997
|
+
output_config=output_config,
|
|
978
998
|
service_client=client,
|
|
979
999
|
)
|
|
980
1000
|
execute(
|
|
@@ -1001,6 +1021,7 @@ def run(
|
|
|
1001
1021
|
location=schema,
|
|
1002
1022
|
base_url=base_url,
|
|
1003
1023
|
started_at=started_at,
|
|
1024
|
+
output_config=output_config,
|
|
1004
1025
|
)
|
|
1005
1026
|
|
|
1006
1027
|
|
|
@@ -1029,6 +1050,7 @@ class LoaderConfig:
|
|
|
1029
1050
|
request_cert: RequestCert | None
|
|
1030
1051
|
wait_for_schema: float | None
|
|
1031
1052
|
rate_limit: str | None
|
|
1053
|
+
output_config: OutputConfig
|
|
1032
1054
|
# Network request parameters
|
|
1033
1055
|
auth: tuple[str, str] | None
|
|
1034
1056
|
auth_type: str | None
|
|
@@ -1072,6 +1094,7 @@ def into_event_stream(
|
|
|
1072
1094
|
workers_num: int,
|
|
1073
1095
|
hypothesis_settings: hypothesis.settings | None,
|
|
1074
1096
|
generation_config: generation.GenerationConfig,
|
|
1097
|
+
output_config: OutputConfig,
|
|
1075
1098
|
seed: int | None,
|
|
1076
1099
|
exit_first: bool,
|
|
1077
1100
|
max_failures: int | None,
|
|
@@ -1105,6 +1128,7 @@ def into_event_stream(
|
|
|
1105
1128
|
method=method or None,
|
|
1106
1129
|
tag=tag or None,
|
|
1107
1130
|
operation_id=operation_id or None,
|
|
1131
|
+
output_config=output_config,
|
|
1108
1132
|
)
|
|
1109
1133
|
schema = load_schema(config)
|
|
1110
1134
|
yield from runner.from_schema(
|
|
@@ -1250,6 +1274,7 @@ def get_loader_kwargs(loader: Callable, config: LoaderConfig) -> dict[str, Any]:
|
|
|
1250
1274
|
"force_schema_version": config.force_schema_version,
|
|
1251
1275
|
"data_generation_methods": config.data_generation_methods,
|
|
1252
1276
|
"rate_limit": config.rate_limit,
|
|
1277
|
+
"output_config": config.output_config,
|
|
1253
1278
|
}
|
|
1254
1279
|
if loader not in (oas_loaders.from_path, oas_loaders.from_dict):
|
|
1255
1280
|
kwargs["headers"] = config.headers
|
|
@@ -1377,6 +1402,7 @@ def execute(
|
|
|
1377
1402
|
location: str,
|
|
1378
1403
|
base_url: str | None,
|
|
1379
1404
|
started_at: str,
|
|
1405
|
+
output_config: OutputConfig,
|
|
1380
1406
|
) -> None:
|
|
1381
1407
|
"""Execute a prepared runner by drawing events from it and passing to a proper handler."""
|
|
1382
1408
|
handlers: list[EventHandler] = []
|
|
@@ -1440,6 +1466,7 @@ def execute(
|
|
|
1440
1466
|
verbosity=verbosity,
|
|
1441
1467
|
code_sample_style=code_sample_style,
|
|
1442
1468
|
report=report_context,
|
|
1469
|
+
output_config=output_config,
|
|
1443
1470
|
)
|
|
1444
1471
|
|
|
1445
1472
|
def shutdown() -> None:
|
schemathesis/cli/callbacks.py
CHANGED
|
@@ -7,25 +7,24 @@ import re
|
|
|
7
7
|
import traceback
|
|
8
8
|
from contextlib import contextmanager
|
|
9
9
|
from functools import partial
|
|
10
|
-
from typing import
|
|
10
|
+
from typing import TYPE_CHECKING, Callable, Generator
|
|
11
11
|
from urllib.parse import urlparse
|
|
12
12
|
|
|
13
13
|
import click
|
|
14
|
-
|
|
15
14
|
from click.types import LazyFile # type: ignore
|
|
16
15
|
|
|
17
16
|
from .. import exceptions, experimental, throttling
|
|
18
17
|
from ..code_samples import CodeSampleStyle
|
|
18
|
+
from ..constants import FALSE_VALUES, TRUE_VALUES
|
|
19
19
|
from ..exceptions import extract_nth_traceback
|
|
20
20
|
from ..generation import DataGenerationMethod
|
|
21
|
-
from ..constants import TRUE_VALUES, FALSE_VALUES
|
|
22
21
|
from ..internal.validation import file_exists, is_filename, is_illegal_surrogate
|
|
23
22
|
from ..loaders import load_app
|
|
24
23
|
from ..service.hosts import get_temporary_hosts_file
|
|
24
|
+
from ..stateful import Stateful
|
|
25
25
|
from ..transports.headers import has_invalid_characters, is_latin_1_encodable
|
|
26
26
|
from ..types import PathLike
|
|
27
27
|
from .constants import DEFAULT_WORKERS
|
|
28
|
-
from ..stateful import Stateful
|
|
29
28
|
|
|
30
29
|
if TYPE_CHECKING:
|
|
31
30
|
import hypothesis
|
schemathesis/cli/cassettes.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import base64
|
|
3
4
|
import json
|
|
4
5
|
import re
|
|
@@ -6,7 +7,7 @@ import sys
|
|
|
6
7
|
import threading
|
|
7
8
|
from dataclasses import dataclass, field
|
|
8
9
|
from queue import Queue
|
|
9
|
-
from typing import IO, Any, Generator, Iterator, cast
|
|
10
|
+
from typing import IO, TYPE_CHECKING, Any, Generator, Iterator, cast
|
|
10
11
|
|
|
11
12
|
from ..constants import SCHEMATHESIS_VERSION
|
|
12
13
|
from ..runner import events
|
|
@@ -16,10 +17,11 @@ from .handlers import EventHandler
|
|
|
16
17
|
if TYPE_CHECKING:
|
|
17
18
|
import click
|
|
18
19
|
import requests
|
|
20
|
+
|
|
21
|
+
from ..generation import DataGenerationMethod
|
|
19
22
|
from ..models import Request, Response
|
|
20
23
|
from ..runner.serialization import SerializedCheck, SerializedInteraction
|
|
21
24
|
from .context import ExecutionContext
|
|
22
|
-
from ..generation import DataGenerationMethod
|
|
23
25
|
|
|
24
26
|
# Wait until the worker terminates
|
|
25
27
|
WRITER_WORKER_JOIN_TIMEOUT = 1
|
|
@@ -351,9 +353,9 @@ def filter_cassette(
|
|
|
351
353
|
|
|
352
354
|
def get_prepared_request(data: dict[str, Any]) -> requests.PreparedRequest:
|
|
353
355
|
"""Create a `requests.PreparedRequest` from a serialized one."""
|
|
354
|
-
from requests.structures import CaseInsensitiveDict
|
|
355
|
-
from requests.cookies import RequestsCookieJar
|
|
356
356
|
import requests
|
|
357
|
+
from requests.cookies import RequestsCookieJar
|
|
358
|
+
from requests.structures import CaseInsensitiveDict
|
|
357
359
|
|
|
358
360
|
prepared = requests.PreparedRequest()
|
|
359
361
|
prepared.method = data["method"]
|
schemathesis/cli/constants.py
CHANGED
schemathesis/cli/context.py
CHANGED
|
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING
|
|
|
8
8
|
|
|
9
9
|
from ..code_samples import CodeSampleStyle
|
|
10
10
|
from ..internal.deprecation import deprecated_property
|
|
11
|
+
from ..internal.output import OutputConfig
|
|
11
12
|
from ..internal.result import Result
|
|
12
13
|
from ..runner.probes import ProbeRun
|
|
13
14
|
from ..runner.serialization import SerializedTestResult
|
|
@@ -16,6 +17,8 @@ from ..service.models import AnalysisResult
|
|
|
16
17
|
if TYPE_CHECKING:
|
|
17
18
|
import hypothesis
|
|
18
19
|
|
|
20
|
+
from ..stateful.sink import StateMachineSink
|
|
21
|
+
|
|
19
22
|
|
|
20
23
|
@dataclass
|
|
21
24
|
class ServiceReportContext:
|
|
@@ -55,8 +58,10 @@ class ExecutionContext:
|
|
|
55
58
|
report: ServiceReportContext | FileReportContext | None = None
|
|
56
59
|
probes: list[ProbeRun] | None = None
|
|
57
60
|
analysis: Result[AnalysisResult, Exception] | None = None
|
|
61
|
+
output_config: OutputConfig = field(default_factory=OutputConfig)
|
|
58
62
|
# Special flag to display a warning about Windows-specific encoding issue
|
|
59
63
|
encountered_windows_encoding_issue: bool = False
|
|
64
|
+
state_machine_sink: StateMachineSink | None = None
|
|
60
65
|
|
|
61
66
|
@deprecated_property(removed_in="4.0", replacement="show_trace")
|
|
62
67
|
def show_errors_tracebacks(self) -> bool:
|
schemathesis/cli/debug.py
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import json
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from typing import TYPE_CHECKING
|
|
5
6
|
|
|
6
|
-
|
|
7
7
|
from .handlers import EventHandler
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
10
|
from click.utils import LazyFile
|
|
11
|
+
|
|
11
12
|
from ..runner import events
|
|
12
13
|
from .context import ExecutionContext
|
|
13
14
|
|
schemathesis/cli/handlers.py
CHANGED
schemathesis/cli/junitxml.py
CHANGED
|
@@ -7,7 +7,8 @@ from typing import TYPE_CHECKING, cast
|
|
|
7
7
|
|
|
8
8
|
from junit_xml import TestCase, TestSuite, to_xml_report_file
|
|
9
9
|
|
|
10
|
-
from ..exceptions import RuntimeErrorType
|
|
10
|
+
from ..exceptions import RuntimeErrorType
|
|
11
|
+
from ..internal.output import prepare_response_payload
|
|
11
12
|
from ..models import Status
|
|
12
13
|
from ..runner import events
|
|
13
14
|
from ..runner.serialization import SerializedCheck, SerializedError
|
|
@@ -37,7 +38,7 @@ class JunitXMLHandler(EventHandler):
|
|
|
37
38
|
group_by_case(event.result.checks, context.code_sample_style), 1
|
|
38
39
|
):
|
|
39
40
|
checks = sorted(group, key=lambda c: c.name != "not_a_server_error")
|
|
40
|
-
test_case.add_failure_info(message=build_failure_message(idx, code_sample, checks))
|
|
41
|
+
test_case.add_failure_info(message=build_failure_message(context, idx, code_sample, checks))
|
|
41
42
|
elif event.status == Status.error:
|
|
42
43
|
test_case.add_error_info(message=build_error_message(context, event.result.errors[-1]))
|
|
43
44
|
elif event.status == Status.skip:
|
|
@@ -48,7 +49,7 @@ class JunitXMLHandler(EventHandler):
|
|
|
48
49
|
to_xml_report_file(file_descriptor=self.file_handle, test_suites=test_suites, prettyprint=True)
|
|
49
50
|
|
|
50
51
|
|
|
51
|
-
def build_failure_message(idx: int, code_sample: str, checks: list[SerializedCheck]) -> str:
|
|
52
|
+
def build_failure_message(context: ExecutionContext, idx: int, code_sample: str, checks: list[SerializedCheck]) -> str:
|
|
52
53
|
from ..transports.responses import get_reason
|
|
53
54
|
|
|
54
55
|
message = ""
|
|
@@ -73,7 +74,7 @@ def build_failure_message(idx: int, code_sample: str, checks: list[SerializedChe
|
|
|
73
74
|
# Checked that is not None
|
|
74
75
|
body = cast(bytes, check.response.deserialize_body())
|
|
75
76
|
payload = body.decode(encoding)
|
|
76
|
-
payload = prepare_response_payload(payload)
|
|
77
|
+
payload = prepare_response_payload(payload, config=context.output_config)
|
|
77
78
|
payload = textwrap.indent(f"\n`{payload}`\n", prefix=" ")
|
|
78
79
|
message += payload
|
|
79
80
|
except UnicodeDecodeError:
|
schemathesis/cli/options.py
CHANGED
|
@@ -7,11 +7,11 @@ import textwrap
|
|
|
7
7
|
import time
|
|
8
8
|
from importlib import metadata
|
|
9
9
|
from queue import Queue
|
|
10
|
-
from typing import Any, Generator,
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Generator, Literal, cast
|
|
11
11
|
|
|
12
12
|
import click
|
|
13
13
|
|
|
14
|
-
from ... import service
|
|
14
|
+
from ... import experimental, service
|
|
15
15
|
from ...constants import (
|
|
16
16
|
DISCORD_LINK,
|
|
17
17
|
FALSE_VALUES,
|
|
@@ -24,21 +24,23 @@ from ...constants import (
|
|
|
24
24
|
)
|
|
25
25
|
from ...exceptions import (
|
|
26
26
|
RuntimeErrorType,
|
|
27
|
-
format_exception,
|
|
28
|
-
prepare_response_payload,
|
|
29
27
|
extract_requests_exception_details,
|
|
28
|
+
format_exception,
|
|
30
29
|
)
|
|
31
30
|
from ...experimental import GLOBAL_EXPERIMENTS
|
|
31
|
+
from ...internal.output import prepare_response_payload
|
|
32
32
|
from ...internal.result import Ok
|
|
33
33
|
from ...models import Status
|
|
34
34
|
from ...runner import events
|
|
35
35
|
from ...runner.events import InternalErrorType, SchemaErrorType
|
|
36
36
|
from ...runner.probes import ProbeOutcome
|
|
37
37
|
from ...runner.serialization import SerializedError, SerializedTestResult
|
|
38
|
-
from ...service.models import AnalysisSuccess,
|
|
38
|
+
from ...service.models import AnalysisSuccess, ErrorState, UnknownExtension
|
|
39
|
+
from ...stateful import events as stateful_events
|
|
40
|
+
from ...stateful.sink import StateMachineSink
|
|
39
41
|
from ..context import ExecutionContext, FileReportContext, ServiceReportContext
|
|
40
42
|
from ..handlers import EventHandler
|
|
41
|
-
from ..reporting import
|
|
43
|
+
from ..reporting import TEST_CASE_ID_TITLE, get_runtime_error_suggestion, group_by_case, split_traceback
|
|
42
44
|
|
|
43
45
|
if TYPE_CHECKING:
|
|
44
46
|
import requests
|
|
@@ -68,14 +70,14 @@ def get_percentage(position: int, length: int) -> str:
|
|
|
68
70
|
return f"[{percentage_message}]"
|
|
69
71
|
|
|
70
72
|
|
|
71
|
-
def display_execution_result(context: ExecutionContext,
|
|
73
|
+
def display_execution_result(context: ExecutionContext, status: Literal["success", "failure", "error", "skip"]) -> None:
|
|
72
74
|
"""Display an appropriate symbol for the given event's execution result."""
|
|
73
75
|
symbol, color = {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}[
|
|
76
|
+
"success": (".", "green"),
|
|
77
|
+
"failure": ("F", "red"),
|
|
78
|
+
"error": ("E", "red"),
|
|
79
|
+
"skip": ("S", "yellow"),
|
|
80
|
+
}[status]
|
|
79
81
|
context.current_line_length += len(symbol)
|
|
80
82
|
click.secho(symbol, nl=False, fg=color)
|
|
81
83
|
|
|
@@ -312,7 +314,7 @@ def display_failures_for_single_test(context: ExecutionContext, result: Serializ
|
|
|
312
314
|
# Checked that is not None
|
|
313
315
|
body = cast(bytes, check.response.deserialize_body())
|
|
314
316
|
payload = body.decode(encoding)
|
|
315
|
-
payload = prepare_response_payload(payload)
|
|
317
|
+
payload = prepare_response_payload(payload, config=context.output_config)
|
|
316
318
|
payload = textwrap.indent(f"\n`{payload}`", prefix=" ")
|
|
317
319
|
click.echo(payload)
|
|
318
320
|
except UnicodeDecodeError:
|
|
@@ -433,6 +435,9 @@ def display_statistic(context: ExecutionContext, event: events.Finished) -> None
|
|
|
433
435
|
display_section_name("SUMMARY")
|
|
434
436
|
click.echo()
|
|
435
437
|
total = event.total
|
|
438
|
+
if context.state_machine_sink is not None:
|
|
439
|
+
click.echo(context.state_machine_sink.transitions.to_formatted_table(get_terminal_width()))
|
|
440
|
+
click.echo()
|
|
436
441
|
if event.is_empty or not total:
|
|
437
442
|
click.secho("No checks were performed.", bold=True)
|
|
438
443
|
|
|
@@ -551,7 +556,7 @@ def display_report_metadata(meta: service.Metadata) -> None:
|
|
|
551
556
|
if value is not None:
|
|
552
557
|
click.secho(f" -> {key}: {value}")
|
|
553
558
|
click.echo()
|
|
554
|
-
click.secho(f"Compressed report size: {meta.size / 1024
|
|
559
|
+
click.secho(f"Compressed report size: {meta.size / 1024.0:,.0f} KB", bold=True)
|
|
555
560
|
|
|
556
561
|
|
|
557
562
|
def display_service_unauthorized(hostname: str) -> None:
|
|
@@ -839,7 +844,7 @@ def handle_after_execution(context: ExecutionContext, event: events.AfterExecuti
|
|
|
839
844
|
"""Display the execution result + current progress at the same line with the method / path names."""
|
|
840
845
|
context.operations_processed += 1
|
|
841
846
|
context.results.append(event.result)
|
|
842
|
-
display_execution_result(context, event)
|
|
847
|
+
display_execution_result(context, event.status.value)
|
|
843
848
|
display_percentage(context, event)
|
|
844
849
|
|
|
845
850
|
|
|
@@ -867,27 +872,54 @@ def handle_internal_error(context: ExecutionContext, event: events.InternalError
|
|
|
867
872
|
raise click.Abort
|
|
868
873
|
|
|
869
874
|
|
|
875
|
+
def handle_stateful_event(context: ExecutionContext, event: events.StatefulEvent) -> None:
|
|
876
|
+
if isinstance(event.data, stateful_events.RunStarted):
|
|
877
|
+
context.state_machine_sink = event.data.state_machine.sink()
|
|
878
|
+
if not experimental.STATEFUL_ONLY.is_enabled:
|
|
879
|
+
click.echo()
|
|
880
|
+
click.secho("Stateful tests\n", bold=True)
|
|
881
|
+
elif (
|
|
882
|
+
isinstance(event.data, stateful_events.ScenarioFinished)
|
|
883
|
+
and not event.data.is_final
|
|
884
|
+
and event.data.status != stateful_events.ScenarioStatus.REJECTED
|
|
885
|
+
):
|
|
886
|
+
display_execution_result(context, event.data.status.value)
|
|
887
|
+
elif isinstance(event.data, stateful_events.RunFinished):
|
|
888
|
+
click.echo()
|
|
889
|
+
# It is initialized in `RunStarted`
|
|
890
|
+
sink = cast(StateMachineSink, context.state_machine_sink)
|
|
891
|
+
sink.consume(event.data)
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
def handle_after_stateful_execution(context: ExecutionContext, event: events.AfterStatefulExecution) -> None:
|
|
895
|
+
context.results.append(event.result)
|
|
896
|
+
|
|
897
|
+
|
|
870
898
|
class DefaultOutputStyleHandler(EventHandler):
|
|
871
899
|
def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
|
|
872
900
|
"""Choose and execute a proper handler for the given event."""
|
|
873
901
|
if isinstance(event, events.Initialized):
|
|
874
902
|
handle_initialized(context, event)
|
|
875
|
-
|
|
903
|
+
elif isinstance(event, events.BeforeProbing):
|
|
876
904
|
handle_before_probing(context, event)
|
|
877
|
-
|
|
905
|
+
elif isinstance(event, events.AfterProbing):
|
|
878
906
|
handle_after_probing(context, event)
|
|
879
|
-
|
|
907
|
+
elif isinstance(event, events.BeforeAnalysis):
|
|
880
908
|
handle_before_analysis(context, event)
|
|
881
|
-
|
|
909
|
+
elif isinstance(event, events.AfterAnalysis):
|
|
882
910
|
handle_after_analysis(context, event)
|
|
883
|
-
|
|
911
|
+
elif isinstance(event, events.BeforeExecution):
|
|
884
912
|
handle_before_execution(context, event)
|
|
885
|
-
|
|
913
|
+
elif isinstance(event, events.AfterExecution):
|
|
886
914
|
context.hypothesis_output.extend(event.hypothesis_output)
|
|
887
915
|
handle_after_execution(context, event)
|
|
888
|
-
|
|
916
|
+
elif isinstance(event, events.Finished):
|
|
889
917
|
handle_finished(context, event)
|
|
890
|
-
|
|
918
|
+
elif isinstance(event, events.Interrupted):
|
|
891
919
|
handle_interrupted(context, event)
|
|
892
|
-
|
|
920
|
+
elif isinstance(event, events.InternalError):
|
|
893
921
|
handle_internal_error(context, event)
|
|
922
|
+
elif isinstance(event, events.StatefulEvent):
|
|
923
|
+
handle_stateful_event(context, event)
|
|
924
|
+
elif isinstance(event, events.AfterStatefulExecution):
|
|
925
|
+
handle_after_stateful_execution(context, event)
|