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.
Files changed (125) hide show
  1. schemathesis/__init__.py +3 -3
  2. schemathesis/_compat.py +2 -2
  3. schemathesis/_dependency_versions.py +1 -3
  4. schemathesis/_hypothesis.py +6 -0
  5. schemathesis/_lazy_import.py +1 -0
  6. schemathesis/_override.py +1 -0
  7. schemathesis/_rate_limiter.py +2 -1
  8. schemathesis/_xml.py +1 -0
  9. schemathesis/auths.py +4 -2
  10. schemathesis/checks.py +8 -5
  11. schemathesis/cli/__init__.py +28 -1
  12. schemathesis/cli/callbacks.py +3 -4
  13. schemathesis/cli/cassettes.py +6 -4
  14. schemathesis/cli/constants.py +2 -0
  15. schemathesis/cli/context.py +5 -0
  16. schemathesis/cli/debug.py +2 -1
  17. schemathesis/cli/handlers.py +1 -1
  18. schemathesis/cli/junitxml.py +5 -4
  19. schemathesis/cli/options.py +1 -0
  20. schemathesis/cli/output/default.py +56 -24
  21. schemathesis/cli/output/short.py +21 -10
  22. schemathesis/cli/sanitization.py +1 -0
  23. schemathesis/code_samples.py +1 -0
  24. schemathesis/constants.py +1 -0
  25. schemathesis/contrib/openapi/__init__.py +1 -1
  26. schemathesis/contrib/openapi/fill_missing_examples.py +2 -0
  27. schemathesis/contrib/openapi/formats/uuid.py +2 -1
  28. schemathesis/contrib/unique_data.py +2 -1
  29. schemathesis/exceptions.py +42 -61
  30. schemathesis/experimental/__init__.py +14 -0
  31. schemathesis/extra/_aiohttp.py +1 -0
  32. schemathesis/extra/_server.py +1 -0
  33. schemathesis/extra/pytest_plugin.py +13 -24
  34. schemathesis/failures.py +42 -8
  35. schemathesis/filters.py +2 -1
  36. schemathesis/fixups/__init__.py +1 -0
  37. schemathesis/fixups/fast_api.py +2 -2
  38. schemathesis/fixups/utf8_bom.py +1 -2
  39. schemathesis/generation/__init__.py +2 -1
  40. schemathesis/hooks.py +3 -1
  41. schemathesis/internal/copy.py +19 -3
  42. schemathesis/internal/deprecation.py +1 -1
  43. schemathesis/internal/jsonschema.py +2 -1
  44. schemathesis/internal/output.py +68 -0
  45. schemathesis/internal/result.py +1 -1
  46. schemathesis/internal/transformation.py +1 -0
  47. schemathesis/lazy.py +11 -2
  48. schemathesis/loaders.py +4 -2
  49. schemathesis/models.py +22 -7
  50. schemathesis/parameters.py +1 -0
  51. schemathesis/runner/__init__.py +1 -1
  52. schemathesis/runner/events.py +22 -4
  53. schemathesis/runner/impl/core.py +69 -33
  54. schemathesis/runner/impl/solo.py +2 -1
  55. schemathesis/runner/impl/threadpool.py +4 -0
  56. schemathesis/runner/probes.py +1 -1
  57. schemathesis/runner/serialization.py +1 -1
  58. schemathesis/sanitization.py +2 -0
  59. schemathesis/schemas.py +7 -4
  60. schemathesis/service/ci.py +1 -0
  61. schemathesis/service/client.py +7 -7
  62. schemathesis/service/events.py +2 -1
  63. schemathesis/service/extensions.py +5 -5
  64. schemathesis/service/hosts.py +1 -0
  65. schemathesis/service/metadata.py +2 -1
  66. schemathesis/service/models.py +2 -1
  67. schemathesis/service/report.py +3 -3
  68. schemathesis/service/serialization.py +62 -23
  69. schemathesis/service/usage.py +1 -0
  70. schemathesis/specs/graphql/_cache.py +1 -1
  71. schemathesis/specs/graphql/loaders.py +17 -1
  72. schemathesis/specs/graphql/nodes.py +1 -0
  73. schemathesis/specs/graphql/scalars.py +2 -2
  74. schemathesis/specs/graphql/schemas.py +7 -7
  75. schemathesis/specs/graphql/validation.py +1 -2
  76. schemathesis/specs/openapi/_hypothesis.py +17 -11
  77. schemathesis/specs/openapi/checks.py +102 -9
  78. schemathesis/specs/openapi/converter.py +2 -1
  79. schemathesis/specs/openapi/definitions.py +2 -1
  80. schemathesis/specs/openapi/examples.py +7 -9
  81. schemathesis/specs/openapi/expressions/__init__.py +29 -2
  82. schemathesis/specs/openapi/expressions/context.py +1 -1
  83. schemathesis/specs/openapi/expressions/extractors.py +23 -0
  84. schemathesis/specs/openapi/expressions/lexer.py +19 -18
  85. schemathesis/specs/openapi/expressions/nodes.py +24 -4
  86. schemathesis/specs/openapi/expressions/parser.py +26 -5
  87. schemathesis/specs/openapi/filters.py +1 -0
  88. schemathesis/specs/openapi/links.py +35 -7
  89. schemathesis/specs/openapi/loaders.py +31 -11
  90. schemathesis/specs/openapi/negative/__init__.py +2 -1
  91. schemathesis/specs/openapi/negative/mutations.py +1 -0
  92. schemathesis/specs/openapi/parameters.py +1 -0
  93. schemathesis/specs/openapi/schemas.py +28 -39
  94. schemathesis/specs/openapi/security.py +1 -0
  95. schemathesis/specs/openapi/serialization.py +1 -0
  96. schemathesis/specs/openapi/stateful/__init__.py +159 -70
  97. schemathesis/specs/openapi/stateful/statistic.py +198 -0
  98. schemathesis/specs/openapi/stateful/types.py +13 -0
  99. schemathesis/specs/openapi/utils.py +1 -0
  100. schemathesis/specs/openapi/validation.py +1 -0
  101. schemathesis/stateful/__init__.py +4 -2
  102. schemathesis/stateful/config.py +66 -0
  103. schemathesis/stateful/context.py +103 -0
  104. schemathesis/stateful/events.py +215 -0
  105. schemathesis/stateful/runner.py +238 -0
  106. schemathesis/stateful/sink.py +68 -0
  107. schemathesis/stateful/state_machine.py +39 -22
  108. schemathesis/stateful/statistic.py +20 -0
  109. schemathesis/stateful/validation.py +66 -0
  110. schemathesis/targets.py +1 -0
  111. schemathesis/throttling.py +23 -3
  112. schemathesis/transports/__init__.py +28 -10
  113. schemathesis/transports/auth.py +1 -0
  114. schemathesis/transports/content_types.py +1 -1
  115. schemathesis/transports/headers.py +2 -1
  116. schemathesis/transports/responses.py +6 -4
  117. schemathesis/types.py +1 -0
  118. schemathesis/utils.py +1 -0
  119. {schemathesis-3.29.2.dist-info → schemathesis-3.30.1.dist-info}/METADATA +3 -3
  120. schemathesis-3.30.1.dist-info/RECORD +151 -0
  121. schemathesis/specs/openapi/stateful/links.py +0 -92
  122. schemathesis-3.29.2.dist-info/RECORD +0 -141
  123. {schemathesis-3.29.2.dist-info → schemathesis-3.30.1.dist-info}/WHEEL +0 -0
  124. {schemathesis-3.29.2.dist-info → schemathesis-3.30.1.dist-info}/entry_points.txt +0 -0
  125. {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, experimental, contrib, fixups, graphql, hooks, runner, serializers, targets # noqa: E402
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,6 +1,6 @@
1
- from typing import Any, Type, Callable
2
- from ._lazy_import import lazy_import
1
+ from typing import Any, Callable, Type
3
2
 
3
+ from ._lazy_import import lazy_import
4
4
 
5
5
  __all__ = [ # noqa: F822
6
6
  "JSONMixin",
@@ -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
 
@@ -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
  *,
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import Any, Callable
3
4
 
4
5
 
schemathesis/_override.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from dataclasses import dataclass
3
4
  from typing import TYPE_CHECKING, Optional
4
5
 
@@ -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, RequestRate as Rate
6
+ from pyrate_limiter import Limiter
7
+ from pyrate_limiter import RequestRate as Rate
schemathesis/_xml.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """XML serialization."""
2
2
 
3
3
  from __future__ import annotations
4
+
4
5
  from io import StringIO
5
6
  from typing import Any, Dict, List, Union
6
7
  from xml.etree import ElementTree
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
- from typing import TYPE_CHECKING
2
+
3
3
  import json
4
+ from typing import TYPE_CHECKING
4
5
 
5
6
  from . import failures
6
- from .exceptions import get_server_error, get_response_parsing_error
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
 
@@ -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([experimental.OPEN_API_3_1.name, experimental.SCHEMA_ANALYSIS.name]),
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:
@@ -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 Generator, TYPE_CHECKING, Callable
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
@@ -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, TYPE_CHECKING
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"]
@@ -1,9 +1,11 @@
1
1
  from __future__ import annotations
2
+
2
3
  from enum import IntEnum, unique
3
4
  from typing import TYPE_CHECKING
4
5
 
5
6
  if TYPE_CHECKING:
6
7
  import hypothesis
8
+
7
9
  MIN_WORKERS = 1
8
10
  DEFAULT_WORKERS = MIN_WORKERS
9
11
  MAX_WORKERS = 64
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
- from typing import TYPE_CHECKING
3
2
 
3
+ from typing import TYPE_CHECKING
4
4
 
5
5
  if TYPE_CHECKING:
6
6
  from ..runner import events
@@ -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, prepare_response_payload
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:
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from enum import Enum
3
4
  from typing import Any, NoReturn
4
5
 
@@ -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, cast, TYPE_CHECKING
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, UnknownExtension, ErrorState
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 group_by_case, TEST_CASE_ID_TITLE, split_traceback, get_runtime_error_suggestion
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, event: events.AfterExecution) -> None:
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
- Status.success: (".", "green"),
75
- Status.failure: ("F", "red"),
76
- Status.error: ("E", "red"),
77
- Status.skip: ("S", "yellow"),
78
- }[event.status]
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.:,.0f} KB", bold=True)
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
- if isinstance(event, events.BeforeProbing):
903
+ elif isinstance(event, events.BeforeProbing):
876
904
  handle_before_probing(context, event)
877
- if isinstance(event, events.AfterProbing):
905
+ elif isinstance(event, events.AfterProbing):
878
906
  handle_after_probing(context, event)
879
- if isinstance(event, events.BeforeAnalysis):
907
+ elif isinstance(event, events.BeforeAnalysis):
880
908
  handle_before_analysis(context, event)
881
- if isinstance(event, events.AfterAnalysis):
909
+ elif isinstance(event, events.AfterAnalysis):
882
910
  handle_after_analysis(context, event)
883
- if isinstance(event, events.BeforeExecution):
911
+ elif isinstance(event, events.BeforeExecution):
884
912
  handle_before_execution(context, event)
885
- if isinstance(event, events.AfterExecution):
913
+ elif isinstance(event, events.AfterExecution):
886
914
  context.hypothesis_output.extend(event.hypothesis_output)
887
915
  handle_after_execution(context, event)
888
- if isinstance(event, events.Finished):
916
+ elif isinstance(event, events.Finished):
889
917
  handle_finished(context, event)
890
- if isinstance(event, events.Interrupted):
918
+ elif isinstance(event, events.Interrupted):
891
919
  handle_interrupted(context, event)
892
- if isinstance(event, events.InternalError):
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)