schemathesis 4.0.0a11__py3-none-any.whl → 4.0.0b1__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 (73) hide show
  1. schemathesis/__init__.py +35 -27
  2. schemathesis/auths.py +85 -54
  3. schemathesis/checks.py +65 -36
  4. schemathesis/cli/commands/run/__init__.py +32 -27
  5. schemathesis/cli/commands/run/context.py +6 -1
  6. schemathesis/cli/commands/run/events.py +7 -1
  7. schemathesis/cli/commands/run/executor.py +12 -7
  8. schemathesis/cli/commands/run/handlers/output.py +188 -80
  9. schemathesis/cli/commands/run/validation.py +21 -6
  10. schemathesis/cli/constants.py +1 -1
  11. schemathesis/config/__init__.py +2 -1
  12. schemathesis/config/_generation.py +12 -13
  13. schemathesis/config/_operations.py +14 -0
  14. schemathesis/config/_phases.py +41 -5
  15. schemathesis/config/_projects.py +33 -1
  16. schemathesis/config/_report.py +6 -2
  17. schemathesis/config/_warnings.py +25 -0
  18. schemathesis/config/schema.json +49 -1
  19. schemathesis/core/errors.py +15 -19
  20. schemathesis/core/transport.py +117 -2
  21. schemathesis/engine/context.py +1 -0
  22. schemathesis/engine/errors.py +61 -2
  23. schemathesis/engine/events.py +10 -2
  24. schemathesis/engine/phases/probes.py +3 -0
  25. schemathesis/engine/phases/stateful/__init__.py +2 -1
  26. schemathesis/engine/phases/stateful/_executor.py +38 -5
  27. schemathesis/engine/phases/stateful/context.py +2 -2
  28. schemathesis/engine/phases/unit/_executor.py +36 -7
  29. schemathesis/generation/__init__.py +0 -3
  30. schemathesis/generation/case.py +153 -28
  31. schemathesis/generation/coverage.py +1 -1
  32. schemathesis/generation/hypothesis/builder.py +43 -19
  33. schemathesis/generation/metrics.py +93 -0
  34. schemathesis/generation/modes.py +0 -8
  35. schemathesis/generation/overrides.py +11 -27
  36. schemathesis/generation/stateful/__init__.py +17 -0
  37. schemathesis/generation/stateful/state_machine.py +32 -108
  38. schemathesis/graphql/loaders.py +152 -8
  39. schemathesis/hooks.py +63 -39
  40. schemathesis/openapi/checks.py +82 -20
  41. schemathesis/openapi/generation/filters.py +9 -2
  42. schemathesis/openapi/loaders.py +134 -8
  43. schemathesis/pytest/lazy.py +4 -31
  44. schemathesis/pytest/loaders.py +24 -0
  45. schemathesis/pytest/plugin.py +38 -6
  46. schemathesis/schemas.py +161 -94
  47. schemathesis/specs/graphql/scalars.py +37 -3
  48. schemathesis/specs/graphql/schemas.py +18 -9
  49. schemathesis/specs/openapi/_hypothesis.py +53 -34
  50. schemathesis/specs/openapi/checks.py +111 -47
  51. schemathesis/specs/openapi/expressions/nodes.py +1 -1
  52. schemathesis/specs/openapi/formats.py +30 -3
  53. schemathesis/specs/openapi/media_types.py +44 -1
  54. schemathesis/specs/openapi/negative/__init__.py +5 -3
  55. schemathesis/specs/openapi/negative/mutations.py +2 -2
  56. schemathesis/specs/openapi/parameters.py +0 -3
  57. schemathesis/specs/openapi/schemas.py +14 -93
  58. schemathesis/specs/openapi/stateful/__init__.py +2 -1
  59. schemathesis/specs/openapi/stateful/links.py +1 -63
  60. schemathesis/transport/__init__.py +54 -16
  61. schemathesis/transport/prepare.py +31 -7
  62. schemathesis/transport/requests.py +21 -9
  63. schemathesis/transport/serialization.py +0 -4
  64. schemathesis/transport/wsgi.py +15 -8
  65. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0b1.dist-info}/METADATA +45 -87
  66. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0b1.dist-info}/RECORD +69 -71
  67. schemathesis/contrib/__init__.py +0 -9
  68. schemathesis/contrib/openapi/__init__.py +0 -9
  69. schemathesis/contrib/openapi/fill_missing_examples.py +0 -20
  70. schemathesis/generation/targets.py +0 -69
  71. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0b1.dist-info}/WHEEL +0 -0
  72. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0b1.dist-info}/entry_points.txt +0 -0
  73. {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import textwrap
4
- from dataclasses import dataclass, field
5
4
  from typing import TYPE_CHECKING, Any
6
5
 
7
6
  from schemathesis.config import OutputConfig
@@ -12,24 +11,6 @@ if TYPE_CHECKING:
12
11
  from jsonschema import ValidationError
13
12
 
14
13
 
15
- @dataclass
16
- class NegativeDataRejectionConfig:
17
- # 5xx will pass through
18
- allowed_statuses: list[str] = field(
19
- default_factory=lambda: ["400", "401", "403", "404", "406", "422", "428", "5xx"]
20
- )
21
-
22
-
23
- @dataclass
24
- class PositiveDataAcceptanceConfig:
25
- allowed_statuses: list[str] = field(default_factory=lambda: ["2xx", "401", "403", "404"])
26
-
27
-
28
- @dataclass
29
- class MissingRequiredHeaderConfig:
30
- allowed_statuses: list[str] = field(default_factory=lambda: ["406"])
31
-
32
-
33
14
  class UndefinedStatusCode(Failure):
34
15
  """Response has a status code that is not defined in the schema."""
35
16
 
@@ -323,7 +304,7 @@ class IgnoredAuth(Failure):
323
304
  *,
324
305
  operation: str,
325
306
  message: str,
326
- title: str = "Authentication declared but not enforced",
307
+ title: str = "API accepts requests without authentication",
327
308
  case_id: str | None = None,
328
309
  ) -> None:
329
310
  self.operation = operation
@@ -391,3 +372,84 @@ class RejectedPositiveData(Failure):
391
372
  @property
392
373
  def _unique_key(self) -> str:
393
374
  return str(self.status_code)
375
+
376
+
377
+ class MissingHeaderNotRejected(Failure):
378
+ """API did not reject request without required header."""
379
+
380
+ __slots__ = (
381
+ "operation",
382
+ "header_name",
383
+ "status_code",
384
+ "expected_statuses",
385
+ "message",
386
+ "title",
387
+ "case_id",
388
+ "severity",
389
+ )
390
+
391
+ def __init__(
392
+ self,
393
+ *,
394
+ operation: str,
395
+ header_name: str,
396
+ status_code: int,
397
+ expected_statuses: list[int],
398
+ message: str,
399
+ title: str = "Missing header not rejected",
400
+ case_id: str | None = None,
401
+ ) -> None:
402
+ self.operation = operation
403
+ self.header_name = header_name
404
+ self.status_code = status_code
405
+ self.expected_statuses = expected_statuses
406
+ self.message = message
407
+ self.title = title
408
+ self.case_id = case_id
409
+ self.severity = Severity.MEDIUM
410
+
411
+ @property
412
+ def _unique_key(self) -> str:
413
+ return self.header_name
414
+
415
+
416
+ class UnsupportedMethodResponse(Failure):
417
+ """API response for unsupported HTTP method is incorrect."""
418
+
419
+ __slots__ = (
420
+ "operation",
421
+ "method",
422
+ "status_code",
423
+ "allow_header_present",
424
+ "failure_reason",
425
+ "message",
426
+ "title",
427
+ "case_id",
428
+ "severity",
429
+ )
430
+
431
+ def __init__(
432
+ self,
433
+ *,
434
+ operation: str,
435
+ method: str,
436
+ status_code: int,
437
+ allow_header_present: bool | None = None,
438
+ failure_reason: str, # "wrong_status" or "missing_allow_header"
439
+ message: str,
440
+ title: str = "Unsupported method incorrect response",
441
+ case_id: str | None = None,
442
+ ) -> None:
443
+ self.operation = operation
444
+ self.method = method
445
+ self.status_code = status_code
446
+ self.allow_header_present = allow_header_present
447
+ self.failure_reason = failure_reason
448
+ self.message = message
449
+ self.title = title
450
+ self.case_id = case_id
451
+ self.severity = Severity.MEDIUM
452
+
453
+ @property
454
+ def _unique_key(self) -> str:
455
+ return self.failure_reason
@@ -29,8 +29,15 @@ def is_invalid_path_parameter(value: Any) -> bool:
29
29
  return (
30
30
  value in ("/", "")
31
31
  or contains_unicode_surrogate_pair(value)
32
- or isinstance(value, str)
33
- and ("/" in value or "}" in value or "{" in value)
32
+ or (
33
+ isinstance(value, str)
34
+ and (
35
+ ("/" in value or "}" in value or "{" in value)
36
+ # Avoid situations when the path parameter contains only NULL bytes
37
+ # Many webservers remove such bytes and as the result, the test can target a different API operation
38
+ or (len(value) == value.count("\x00"))
39
+ )
40
+ )
34
41
  )
35
42
 
36
43
 
@@ -20,15 +20,54 @@ if TYPE_CHECKING:
20
20
 
21
21
 
22
22
  def from_asgi(path: str, app: Any, *, config: SchemathesisConfig | None = None, **kwargs: Any) -> BaseOpenAPISchema:
23
+ """Load OpenAPI schema from an ASGI application.
24
+
25
+ Args:
26
+ path: Relative URL path to the OpenAPI schema endpoint (e.g., "/openapi.json")
27
+ app: ASGI application instance
28
+ config: Custom configuration. If `None`, uses auto-discovered config
29
+ **kwargs: Additional request parameters passed to the ASGI test client
30
+
31
+ Example:
32
+ ```python
33
+ from fastapi import FastAPI
34
+ import schemathesis
35
+
36
+ app = FastAPI()
37
+ schema = schemathesis.openapi.from_asgi("/openapi.json", app)
38
+ ```
39
+
40
+ """
23
41
  require_relative_url(path)
24
42
  client = asgi.get_client(app)
25
43
  response = load_from_url(client.get, url=path, **kwargs)
26
44
  content_type = detect_content_type(headers=response.headers, path=path)
27
45
  schema = load_content(response.text, content_type)
28
- return from_dict(schema=schema, config=config).configure(app=app, location=path)
46
+ loaded = from_dict(schema=schema, config=config)
47
+ loaded.app = app
48
+ loaded.location = path
49
+ return loaded
29
50
 
30
51
 
31
52
  def from_wsgi(path: str, app: Any, *, config: SchemathesisConfig | None = None, **kwargs: Any) -> BaseOpenAPISchema:
53
+ """Load OpenAPI schema from a WSGI application.
54
+
55
+ Args:
56
+ path: Relative URL path to the OpenAPI schema endpoint (e.g., "/openapi.json")
57
+ app: WSGI application instance
58
+ config: Custom configuration. If `None`, uses auto-discovered config
59
+ **kwargs: Additional request parameters passed to the WSGI test client
60
+
61
+ Example:
62
+ ```python
63
+ from flask import Flask
64
+ import schemathesis
65
+
66
+ app = Flask(__name__)
67
+ schema = schemathesis.openapi.from_wsgi("/openapi.json", app)
68
+ ```
69
+
70
+ """
32
71
  require_relative_url(path)
33
72
  prepare_request_kwargs(kwargs)
34
73
  client = wsgi.get_client(app)
@@ -36,33 +75,101 @@ def from_wsgi(path: str, app: Any, *, config: SchemathesisConfig | None = None,
36
75
  raise_for_status(response)
37
76
  content_type = detect_content_type(headers=response.headers, path=path)
38
77
  schema = load_content(response.text, content_type)
39
- return from_dict(schema=schema, config=config).configure(app=app, location=path)
78
+ loaded = from_dict(schema=schema, config=config)
79
+ loaded.app = app
80
+ loaded.location = path
81
+ return loaded
40
82
 
41
83
 
42
84
  def from_url(
43
85
  url: str, *, config: SchemathesisConfig | None = None, wait_for_schema: float | None = None, **kwargs: Any
44
86
  ) -> BaseOpenAPISchema:
45
- """Load from URL."""
87
+ """Load OpenAPI schema from a URL.
88
+
89
+ Args:
90
+ url: Full URL to the OpenAPI schema
91
+ config: Custom configuration. If `None`, uses auto-discovered config
92
+ wait_for_schema: Maximum time in seconds to wait for schema availability
93
+ **kwargs: Additional parameters passed to `requests.get()` (headers, timeout, auth, etc.)
94
+
95
+ Example:
96
+ ```python
97
+ import schemathesis
98
+
99
+ # Basic usage
100
+ schema = schemathesis.openapi.from_url("https://api.example.com/openapi.json")
101
+
102
+ # With authentication and timeout
103
+ schema = schemathesis.openapi.from_url(
104
+ "https://api.example.com/openapi.json",
105
+ headers={"Authorization": "Bearer token"},
106
+ timeout=30,
107
+ wait_for_schema=10.0
108
+ )
109
+ ```
110
+
111
+ """
46
112
  import requests
47
113
 
48
114
  response = load_from_url(requests.get, url=url, wait_for_schema=wait_for_schema, **kwargs)
49
115
  content_type = detect_content_type(headers=response.headers, path=url)
50
116
  schema = load_content(response.text, content_type)
51
- return from_dict(schema=schema, config=config).configure(location=url)
117
+ loaded = from_dict(schema=schema, config=config)
118
+ loaded.location = url
119
+ return loaded
52
120
 
53
121
 
54
122
  def from_path(
55
123
  path: PathLike | str, *, config: SchemathesisConfig | None = None, encoding: str = "utf-8"
56
124
  ) -> BaseOpenAPISchema:
57
- """Load from a filesystem path."""
125
+ """Load OpenAPI schema from a filesystem path.
126
+
127
+ Args:
128
+ path: File path to the OpenAPI schema (supports JSON / YAML)
129
+ config: Custom configuration. If `None`, uses auto-discovered config
130
+ encoding: Text encoding for reading the file
131
+
132
+ Example:
133
+ ```python
134
+ import schemathesis
135
+
136
+ # Load from file
137
+ schema = schemathesis.openapi.from_path("./specs/openapi.yaml")
138
+
139
+ # With custom encoding
140
+ schema = schemathesis.openapi.from_path("./specs/openapi.json", encoding="cp1252")
141
+ ```
142
+
143
+ """
58
144
  with open(path, encoding=encoding) as file:
59
145
  content_type = detect_content_type(headers=None, path=str(path))
60
146
  schema = load_content(file.read(), content_type)
61
- return from_dict(schema=schema, config=config).configure(location=Path(path).absolute().as_uri())
147
+ loaded = from_dict(schema=schema, config=config)
148
+ loaded.location = Path(path).absolute().as_uri()
149
+ return loaded
62
150
 
63
151
 
64
152
  def from_file(file: IO[str] | str, *, config: SchemathesisConfig | None = None) -> BaseOpenAPISchema:
65
- """Load from file-like object or string."""
153
+ """Load OpenAPI schema from a file-like object or string.
154
+
155
+ Args:
156
+ file: File-like object or raw string containing the OpenAPI schema
157
+ config: Custom configuration. If `None`, uses auto-discovered config
158
+
159
+ Example:
160
+ ```python
161
+ import schemathesis
162
+
163
+ # From string
164
+ schema_content = '{"openapi": "3.0.0", "info": {"title": "API"}}'
165
+ schema = schemathesis.openapi.from_file(schema_content)
166
+
167
+ # From file object
168
+ with open("openapi.yaml") as f:
169
+ schema = schemathesis.openapi.from_file(f)
170
+ ```
171
+
172
+ """
66
173
  if isinstance(file, str):
67
174
  data = file
68
175
  else:
@@ -75,7 +182,26 @@ def from_file(file: IO[str] | str, *, config: SchemathesisConfig | None = None)
75
182
 
76
183
 
77
184
  def from_dict(schema: dict[str, Any], *, config: SchemathesisConfig | None = None) -> BaseOpenAPISchema:
78
- """Base loader that others build upon."""
185
+ """Load OpenAPI schema from a dictionary.
186
+
187
+ Args:
188
+ schema: Dictionary containing the parsed OpenAPI schema
189
+ config: Custom configuration. If `None`, uses auto-discovered config
190
+
191
+ Example:
192
+ ```python
193
+ import schemathesis
194
+
195
+ schema_dict = {
196
+ "openapi": "3.0.0",
197
+ "info": {"title": "My API", "version": "1.0.0"},
198
+ "paths": {"/users": {"get": {"responses": {"200": {"description": "OK"}}}}}
199
+ }
200
+
201
+ schema = schemathesis.openapi.from_dict(schema_dict)
202
+ ```
203
+
204
+ """
79
205
  from schemathesis.specs.openapi.schemas import OpenApi30, SwaggerV20
80
206
 
81
207
  if not isinstance(schema, dict):
@@ -12,6 +12,7 @@ from pytest_subtests import SubTests
12
12
  from schemathesis.core.errors import InvalidSchema
13
13
  from schemathesis.core.result import Ok, Result
14
14
  from schemathesis.filters import FilterSet, FilterValue, MatcherFunc, RegexValue, is_deprecated
15
+ from schemathesis.generation import overrides
15
16
  from schemathesis.generation.hypothesis.builder import HypothesisTestConfig, HypothesisTestMode, create_test
16
17
  from schemathesis.generation.hypothesis.given import (
17
18
  GivenArgsMark,
@@ -22,7 +23,6 @@ from schemathesis.generation.hypothesis.given import (
22
23
  merge_given_args,
23
24
  validate_given_args,
24
25
  )
25
- from schemathesis.generation.overrides import Override, OverrideMark, check_no_override_mark
26
26
  from schemathesis.pytest.control_flow import fail_on_no_matches
27
27
  from schemathesis.schemas import BaseSchema
28
28
 
@@ -174,17 +174,10 @@ class LazySchema:
174
174
  node_id = request.node._nodeid
175
175
  settings = getattr(wrapped_test, "_hypothesis_internal_use_settings", None)
176
176
 
177
- as_strategy_kwargs: Callable[[APIOperation], dict[str, Any]] | None = None
177
+ def as_strategy_kwargs(_operation: APIOperation) -> dict[str, Any]:
178
+ override = overrides.for_operation(config=schema.config, operation=_operation)
178
179
 
179
- override = OverrideMark.get(test_func)
180
- if override is not None:
181
-
182
- def as_strategy_kwargs(_operation: APIOperation) -> dict[str, Any]:
183
- nonlocal override
184
-
185
- return {
186
- location: entry for location, entry in override.for_operation(_operation).items() if entry
187
- }
180
+ return {location: entry for location, entry in override.items() if entry}
188
181
 
189
182
  tests = list(
190
183
  get_all_tests(
@@ -224,26 +217,6 @@ class LazySchema:
224
217
  def given(self, *args: GivenInput, **kwargs: GivenInput) -> Callable:
225
218
  return given_proxy(*args, **kwargs)
226
219
 
227
- def override(
228
- self,
229
- *,
230
- query: dict[str, str] | None = None,
231
- headers: dict[str, str] | None = None,
232
- cookies: dict[str, str] | None = None,
233
- path_parameters: dict[str, str] | None = None,
234
- ) -> Callable[[Callable], Callable]:
235
- """Override Open API parameters with fixed values."""
236
-
237
- def _add_override(test: Callable) -> Callable:
238
- check_no_override_mark(test)
239
- override = Override(
240
- query=query or {}, headers=headers or {}, cookies=cookies or {}, path_parameters=path_parameters or {}
241
- )
242
- OverrideMark.set(test, override)
243
- return test
244
-
245
- return _add_override
246
-
247
220
 
248
221
  def _copy_marks(source: Callable, target: Callable) -> None:
249
222
  marks = getattr(source, "pytestmark", [])
@@ -7,6 +7,30 @@ if TYPE_CHECKING:
7
7
 
8
8
 
9
9
  def from_fixture(name: str) -> LazySchema:
10
+ """Create a lazy schema loader that resolves a pytest fixture at test runtime.
11
+
12
+ Args:
13
+ name: Name of the pytest fixture that returns a schema object
14
+
15
+ Example:
16
+ ```python
17
+ import pytest
18
+ import schemathesis
19
+
20
+ @pytest.fixture
21
+ def api_schema():
22
+ return schemathesis.openapi.from_url("https://api.example.com/openapi.json")
23
+
24
+ # Create lazy schema from fixture
25
+ schema = schemathesis.pytest.from_fixture("api_schema")
26
+
27
+ # Use with parametrize to generate tests
28
+ @schema.parametrize()
29
+ def test_api(case):
30
+ case.call_and_validate()
31
+ ```
32
+
33
+ """
10
34
  from schemathesis.pytest.lazy import LazySchema
11
35
 
12
36
  return LazySchema(name)
@@ -21,9 +21,12 @@ from schemathesis.core.errors import (
21
21
  InvalidRegexPattern,
22
22
  InvalidSchema,
23
23
  SerializationNotPossible,
24
+ format_exception,
24
25
  )
26
+ from schemathesis.core.failures import FailureGroup
25
27
  from schemathesis.core.marks import Mark
26
28
  from schemathesis.core.result import Ok, Result
29
+ from schemathesis.generation import overrides
27
30
  from schemathesis.generation.hypothesis.given import (
28
31
  GivenArgsMark,
29
32
  GivenKwargsMark,
@@ -32,7 +35,6 @@ from schemathesis.generation.hypothesis.given import (
32
35
  validate_given_args,
33
36
  )
34
37
  from schemathesis.generation.hypothesis.reporting import ignore_hypothesis_output
35
- from schemathesis.generation.overrides import OverrideMark
36
38
  from schemathesis.pytest.control_flow import fail_on_no_matches
37
39
  from schemathesis.schemas import APIOperation
38
40
 
@@ -108,6 +110,7 @@ class SchemathesisCase(PyCollector):
108
110
  This implementation is based on the original one in pytest, but with slight adjustments
109
111
  to produce tests out of hypothesis ones.
110
112
  """
113
+ from schemathesis.checks import load_all_checks
111
114
  from schemathesis.generation.hypothesis.builder import (
112
115
  HypothesisTestConfig,
113
116
  HypothesisTestMode,
@@ -115,6 +118,8 @@ class SchemathesisCase(PyCollector):
115
118
  make_async_test,
116
119
  )
117
120
 
121
+ load_all_checks()
122
+
118
123
  is_trio_test = False
119
124
  for mark in getattr(self.test_function, "pytestmark", []):
120
125
  if mark.name == "trio":
@@ -126,14 +131,22 @@ class SchemathesisCase(PyCollector):
126
131
  if self.is_invalid_test:
127
132
  funcobj = self.test_function
128
133
  else:
129
- override = OverrideMark.get(self.test_function)
134
+ as_strategy_kwargs = {}
135
+
136
+ auth = self.schema.config.auth_for(operation=operation)
137
+ if auth is not None:
138
+ from requests.auth import _basic_auth_str
139
+
140
+ as_strategy_kwargs["headers"] = {"Authorization": _basic_auth_str(*auth)}
141
+ headers = self.schema.config.headers_for(operation=operation)
142
+ if headers:
143
+ as_strategy_kwargs["headers"] = headers
144
+
145
+ override = overrides.for_operation(operation=operation, config=self.schema.config)
130
146
  if override is not None:
131
- as_strategy_kwargs = {}
132
- for location, entry in override.for_operation(operation).items():
147
+ for location, entry in override.items():
133
148
  if entry:
134
149
  as_strategy_kwargs[location] = entry
135
- else:
136
- as_strategy_kwargs = {}
137
150
  modes = []
138
151
  phases = self.schema.config.phases_for(operation=operation)
139
152
  if phases.examples.enabled:
@@ -148,6 +161,7 @@ class SchemathesisCase(PyCollector):
148
161
  test_func=self.test_function,
149
162
  config=HypothesisTestConfig(
150
163
  modes=modes,
164
+ settings=self.schema.config.get_hypothesis_settings(operation=operation),
151
165
  given_kwargs=self.given_kwargs,
152
166
  project=self.schema.config,
153
167
  as_strategy_kwargs=as_strategy_kwargs,
@@ -247,6 +261,24 @@ def pytest_pycollect_makeitem(collector: nodes.Collector, name: str, obj: Any) -
247
261
  outcome.get_result()
248
262
 
249
263
 
264
+ @pytest.hookimpl(tryfirst=True) # type: ignore[misc]
265
+ def pytest_exception_interact(node: Function, call: pytest.CallInfo, report: pytest.TestReport) -> None:
266
+ if call.excinfo and call.excinfo.type is FailureGroup:
267
+ tb_entries = list(call.excinfo.traceback)
268
+ total_frames = len(tb_entries)
269
+
270
+ # Keep internal Schemathesis frames + one extra one from the caller
271
+ skip_frames = 0
272
+ for i in range(total_frames - 1, -1, -1):
273
+ entry = tb_entries[i]
274
+
275
+ if not str(entry.path).endswith("schemathesis/generation/case.py"):
276
+ skip_frames = i
277
+ break
278
+
279
+ report.longrepr = "".join(format_exception(call.excinfo.value, with_traceback=True, skip_frames=skip_frames))
280
+
281
+
250
282
  @hookimpl(wrapper=True)
251
283
  def pytest_pyfunc_call(pyfuncitem): # type:ignore
252
284
  """It is possible to have a Hypothesis exception in runtime.