schemathesis 4.0.0a11__py3-none-any.whl → 4.0.0a12__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 +28 -25
- schemathesis/auths.py +65 -24
- schemathesis/checks.py +60 -36
- schemathesis/cli/commands/run/__init__.py +23 -21
- schemathesis/cli/commands/run/context.py +6 -1
- schemathesis/cli/commands/run/events.py +7 -1
- schemathesis/cli/commands/run/executor.py +12 -7
- schemathesis/cli/commands/run/handlers/output.py +175 -80
- schemathesis/cli/commands/run/validation.py +21 -6
- schemathesis/config/__init__.py +2 -1
- schemathesis/config/_generation.py +12 -13
- schemathesis/config/_operations.py +14 -0
- schemathesis/config/_phases.py +41 -5
- schemathesis/config/_projects.py +28 -0
- schemathesis/config/_report.py +6 -2
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +49 -1
- schemathesis/core/errors.py +5 -2
- schemathesis/core/transport.py +36 -1
- schemathesis/engine/context.py +1 -0
- schemathesis/engine/errors.py +60 -1
- schemathesis/engine/events.py +10 -2
- schemathesis/engine/phases/probes.py +3 -0
- schemathesis/engine/phases/stateful/__init__.py +2 -1
- schemathesis/engine/phases/stateful/_executor.py +38 -5
- schemathesis/engine/phases/stateful/context.py +2 -2
- schemathesis/engine/phases/unit/_executor.py +36 -7
- schemathesis/generation/__init__.py +0 -3
- schemathesis/generation/case.py +1 -0
- schemathesis/generation/coverage.py +1 -1
- schemathesis/generation/hypothesis/builder.py +31 -7
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +0 -8
- schemathesis/generation/stateful/__init__.py +4 -0
- schemathesis/generation/stateful/state_machine.py +1 -0
- schemathesis/graphql/loaders.py +138 -4
- schemathesis/hooks.py +62 -35
- schemathesis/openapi/loaders.py +120 -4
- schemathesis/pytest/loaders.py +24 -0
- schemathesis/pytest/plugin.py +22 -0
- schemathesis/schemas.py +9 -6
- schemathesis/specs/graphql/scalars.py +37 -3
- schemathesis/specs/graphql/schemas.py +12 -3
- schemathesis/specs/openapi/_hypothesis.py +14 -20
- schemathesis/specs/openapi/checks.py +21 -18
- schemathesis/specs/openapi/formats.py +30 -3
- schemathesis/specs/openapi/media_types.py +44 -1
- schemathesis/specs/openapi/schemas.py +8 -2
- schemathesis/specs/openapi/stateful/__init__.py +2 -1
- schemathesis/transport/__init__.py +54 -16
- schemathesis/transport/prepare.py +31 -7
- schemathesis/transport/requests.py +9 -8
- schemathesis/transport/wsgi.py +8 -8
- {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0a12.dist-info}/METADATA +44 -90
- {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0a12.dist-info}/RECORD +58 -60
- schemathesis/contrib/__init__.py +0 -9
- schemathesis/contrib/openapi/__init__.py +0 -9
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -20
- schemathesis/generation/targets.py +0 -69
- {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0a12.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0a12.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.0.0a11.dist-info → schemathesis-4.0.0a12.dist-info}/licenses/LICENSE +0 -0
schemathesis/__init__.py
CHANGED
@@ -1,44 +1,47 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from schemathesis import
|
3
|
+
from schemathesis import errors, graphql, openapi, pytest
|
4
|
+
from schemathesis.auths import AuthContext, auth
|
4
5
|
from schemathesis.checks import CheckContext, CheckFunction, check
|
5
|
-
from schemathesis.core.output import sanitization
|
6
6
|
from schemathesis.core.transport import Response
|
7
7
|
from schemathesis.core.version import SCHEMATHESIS_VERSION
|
8
8
|
from schemathesis.generation import GenerationMode
|
9
9
|
from schemathesis.generation.case import Case
|
10
|
-
from schemathesis.generation.
|
11
|
-
from schemathesis.hooks import HookContext
|
10
|
+
from schemathesis.generation.metrics import MetricContext, MetricFunction, metric
|
11
|
+
from schemathesis.hooks import HookContext, hook
|
12
12
|
from schemathesis.schemas import BaseSchema
|
13
|
+
from schemathesis.transport import SerializationContext, serializer
|
13
14
|
|
14
15
|
__version__ = SCHEMATHESIS_VERSION
|
15
16
|
|
16
|
-
# Public API
|
17
|
-
auth = auths.GLOBAL_AUTH_STORAGE
|
18
|
-
hook = hooks.register
|
19
|
-
|
20
17
|
__all__ = [
|
18
|
+
"__version__",
|
19
|
+
# Core data structures
|
21
20
|
"Case",
|
22
|
-
"CheckContext",
|
23
|
-
"CheckFunction",
|
24
|
-
"GenerationMode",
|
25
21
|
"Response",
|
26
|
-
"
|
27
|
-
"TargetFunction",
|
28
|
-
"HookContext",
|
22
|
+
"GenerationMode",
|
29
23
|
"BaseSchema",
|
30
|
-
|
31
|
-
"auth",
|
32
|
-
"check",
|
33
|
-
"contrib",
|
34
|
-
"engine",
|
24
|
+
# Public errors
|
35
25
|
"errors",
|
36
|
-
|
37
|
-
"hook",
|
38
|
-
"hooks",
|
26
|
+
# Spec or usage specific namespaces
|
39
27
|
"openapi",
|
28
|
+
"graphql",
|
40
29
|
"pytest",
|
41
|
-
|
42
|
-
"
|
43
|
-
"
|
30
|
+
# Hooks
|
31
|
+
"hook",
|
32
|
+
"HookContext",
|
33
|
+
# Checks
|
34
|
+
"check",
|
35
|
+
"CheckContext",
|
36
|
+
"CheckFunction",
|
37
|
+
# Auth
|
38
|
+
"auth",
|
39
|
+
"AuthContext",
|
40
|
+
# Targeted Property-based Testing
|
41
|
+
"metric",
|
42
|
+
"MetricContext",
|
43
|
+
"MetricFunction",
|
44
|
+
# Serialization
|
45
|
+
"serializer",
|
46
|
+
"SerializationContext",
|
44
47
|
]
|
schemathesis/auths.py
CHANGED
@@ -34,14 +34,34 @@ Auth = TypeVar("Auth")
|
|
34
34
|
|
35
35
|
@dataclass
|
36
36
|
class AuthContext:
|
37
|
-
"""
|
37
|
+
"""Runtime context passed to authentication providers during token generation.
|
38
|
+
|
39
|
+
Provides access to the current API operation and application instance when
|
40
|
+
auth providers need operation-specific tokens or application state.
|
41
|
+
|
42
|
+
Example:
|
43
|
+
```python
|
44
|
+
@schemathesis.auth()
|
45
|
+
class ContextAwareAuth:
|
46
|
+
def get(self, case, context):
|
47
|
+
# Access operation details
|
48
|
+
if "/admin/" in context.operation.path:
|
49
|
+
return self.get_admin_token()
|
50
|
+
else:
|
51
|
+
return self.get_user_token()
|
52
|
+
|
53
|
+
def set(self, case, data, context):
|
54
|
+
case.headers = {"Authorization": f"Bearer {data}"}
|
55
|
+
```
|
38
56
|
|
39
|
-
:ivar APIOperation operation: API operation that is currently being processed.
|
40
|
-
:ivar app: Optional Python application if the WSGI / ASGI integration is used.
|
41
57
|
"""
|
42
58
|
|
43
59
|
operation: APIOperation
|
60
|
+
"""API operation currently being processed for authentication."""
|
44
61
|
app: Any | None
|
62
|
+
"""Python application instance (ASGI/WSGI app) when using app integration, `None` otherwise."""
|
63
|
+
|
64
|
+
__slots__ = ("operation", "app")
|
45
65
|
|
46
66
|
|
47
67
|
CacheKeyFunction = Callable[["Case", "AuthContext"], Union[str, int]]
|
@@ -289,7 +309,7 @@ class AuthStorage(Generic[Auth]):
|
|
289
309
|
) -> FilterableRegisterAuth | FilterableApplyAuth:
|
290
310
|
if provider_class is not None:
|
291
311
|
return self.apply(provider_class, refresh_interval=refresh_interval, cache_by_key=cache_by_key)
|
292
|
-
return self.
|
312
|
+
return self.auth(refresh_interval=refresh_interval, cache_by_key=cache_by_key)
|
293
313
|
|
294
314
|
def set_from_requests(self, auth: requests.auth.AuthBase) -> FilterableRequestsAuth:
|
295
315
|
"""Use `requests` auth instance as an auth provider."""
|
@@ -333,30 +353,12 @@ class AuthStorage(Generic[Auth]):
|
|
333
353
|
provider = SelectiveAuthProvider(provider, filter_set)
|
334
354
|
self.providers.append(provider)
|
335
355
|
|
336
|
-
def
|
356
|
+
def auth(
|
337
357
|
self,
|
338
358
|
*,
|
339
359
|
refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
|
340
360
|
cache_by_key: CacheKeyFunction | None = None,
|
341
361
|
) -> FilterableRegisterAuth:
|
342
|
-
"""Register a new auth provider.
|
343
|
-
|
344
|
-
.. code-block:: python
|
345
|
-
|
346
|
-
@schemathesis.auth()
|
347
|
-
class TokenAuth:
|
348
|
-
def get(self, context):
|
349
|
-
response = requests.post(
|
350
|
-
"https://example.schemathesis.io/api/token/",
|
351
|
-
json={"username": "demo", "password": "test"},
|
352
|
-
)
|
353
|
-
data = response.json()
|
354
|
-
return data["access_token"]
|
355
|
-
|
356
|
-
def set(self, case, data, context):
|
357
|
-
# Modify `case` the way you need
|
358
|
-
case.headers = {"Authorization": f"Bearer {data}"}
|
359
|
-
"""
|
360
362
|
filter_set = FilterSet()
|
361
363
|
|
362
364
|
def wrapper(provider_class: type[AuthProvider]) -> type[AuthProvider]:
|
@@ -451,5 +453,44 @@ def set_on_case(case: Case, context: AuthContext, auth_storage: AuthStorage | No
|
|
451
453
|
|
452
454
|
# Global auth API
|
453
455
|
GLOBAL_AUTH_STORAGE: AuthStorage = AuthStorage()
|
454
|
-
register = GLOBAL_AUTH_STORAGE.register
|
455
456
|
unregister = GLOBAL_AUTH_STORAGE.unregister
|
457
|
+
|
458
|
+
|
459
|
+
def auth(
|
460
|
+
*,
|
461
|
+
refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
|
462
|
+
cache_by_key: CacheKeyFunction | None = None,
|
463
|
+
) -> FilterableRegisterAuth:
|
464
|
+
"""Register a dynamic authentication provider for APIs with expiring tokens.
|
465
|
+
|
466
|
+
Args:
|
467
|
+
refresh_interval: Seconds between token refreshes. Default is `300`. Use `None` to disable caching
|
468
|
+
cache_by_key: Function to generate cache keys for different auth contexts (e.g., OAuth scopes)
|
469
|
+
|
470
|
+
Example:
|
471
|
+
```python
|
472
|
+
import schemathesis
|
473
|
+
import requests
|
474
|
+
|
475
|
+
@schemathesis.auth()
|
476
|
+
class TokenAuth:
|
477
|
+
def get(self, case, context):
|
478
|
+
\"\"\"Fetch fresh authentication token\"\"\"
|
479
|
+
response = requests.post(
|
480
|
+
"http://localhost:8000/auth/token",
|
481
|
+
json={"username": "demo", "password": "test"}
|
482
|
+
)
|
483
|
+
return response.json()["access_token"]
|
484
|
+
|
485
|
+
def set(self, case, data, context):
|
486
|
+
\"\"\"Apply token to test case headers\"\"\"
|
487
|
+
case.headers = case.headers or {}
|
488
|
+
case.headers["Authorization"] = f"Bearer {data}"
|
489
|
+
```
|
490
|
+
|
491
|
+
"""
|
492
|
+
return GLOBAL_AUTH_STORAGE.auth(refresh_interval=refresh_interval, cache_by_key=cache_by_key)
|
493
|
+
|
494
|
+
|
495
|
+
auth.__dict__ = GLOBAL_AUTH_STORAGE.auth.__dict__
|
496
|
+
auth.set_from_requests = GLOBAL_AUTH_STORAGE.set_from_requests # type: ignore[attr-defined]
|
schemathesis/checks.py
CHANGED
@@ -26,20 +26,21 @@ CheckFunction = Callable[["CheckContext", "Response", "Case"], Optional[bool]]
|
|
26
26
|
|
27
27
|
|
28
28
|
class CheckContext:
|
29
|
-
"""
|
29
|
+
"""Runtime context passed to validation check functions during API testing.
|
30
30
|
|
31
|
-
Provides access to
|
31
|
+
Provides access to configuration for currently checked endpoint.
|
32
32
|
"""
|
33
33
|
|
34
|
-
|
35
|
-
|
36
|
-
|
34
|
+
_override: Override | None
|
35
|
+
_auth: tuple[str, str] | None
|
36
|
+
_headers: CaseInsensitiveDict | None
|
37
37
|
config: ChecksConfig
|
38
|
-
|
39
|
-
|
40
|
-
|
38
|
+
"""Configuration settings for validation checks."""
|
39
|
+
_transport_kwargs: dict[str, Any] | None
|
40
|
+
_recorder: ScenarioRecorder | None
|
41
|
+
_checks: list[CheckFunction]
|
41
42
|
|
42
|
-
__slots__ = ("
|
43
|
+
__slots__ = ("_override", "_auth", "_headers", "config", "_transport_kwargs", "_recorder", "_checks")
|
43
44
|
|
44
45
|
def __init__(
|
45
46
|
self,
|
@@ -50,55 +51,78 @@ class CheckContext:
|
|
50
51
|
transport_kwargs: dict[str, Any] | None,
|
51
52
|
recorder: ScenarioRecorder | None = None,
|
52
53
|
) -> None:
|
53
|
-
self.
|
54
|
-
self.
|
55
|
-
self.
|
54
|
+
self._override = override
|
55
|
+
self._auth = auth
|
56
|
+
self._headers = headers
|
56
57
|
self.config = config
|
57
|
-
self.
|
58
|
-
self.
|
59
|
-
self.
|
58
|
+
self._transport_kwargs = transport_kwargs
|
59
|
+
self._recorder = recorder
|
60
|
+
self._checks = []
|
60
61
|
for check in CHECKS.get_all():
|
61
62
|
name = check.__name__
|
62
63
|
if self.config.get_by_name(name=name).enabled:
|
63
|
-
self.
|
64
|
+
self._checks.append(check)
|
64
65
|
if self.config.max_response_time.enabled:
|
65
|
-
self.
|
66
|
+
self._checks.append(max_response_time)
|
66
67
|
|
67
|
-
def
|
68
|
-
if self.
|
69
|
-
return self.
|
68
|
+
def _find_parent(self, *, case_id: str) -> Case | None:
|
69
|
+
if self._recorder is not None:
|
70
|
+
return self._recorder.find_parent(case_id=case_id)
|
70
71
|
return None
|
71
72
|
|
72
|
-
def
|
73
|
-
if self.
|
74
|
-
yield from self.
|
73
|
+
def _find_related(self, *, case_id: str) -> Iterator[Case]:
|
74
|
+
if self._recorder is not None:
|
75
|
+
yield from self._recorder.find_related(case_id=case_id)
|
75
76
|
|
76
|
-
def
|
77
|
-
if self.
|
78
|
-
return self.
|
77
|
+
def _find_response(self, *, case_id: str) -> Response | None:
|
78
|
+
if self._recorder is not None:
|
79
|
+
return self._recorder.find_response(case_id=case_id)
|
79
80
|
return None
|
80
81
|
|
81
|
-
def
|
82
|
-
if self.
|
83
|
-
self.
|
82
|
+
def _record_case(self, *, parent_id: str, case: Case) -> None:
|
83
|
+
if self._recorder is not None:
|
84
|
+
self._recorder.record_case(parent_id=parent_id, transition=None, case=case)
|
84
85
|
|
85
|
-
def
|
86
|
-
if self.
|
87
|
-
self.
|
86
|
+
def _record_response(self, *, case_id: str, response: Response) -> None:
|
87
|
+
if self._recorder is not None:
|
88
|
+
self._recorder.record_response(case_id=case_id, response=response)
|
88
89
|
|
89
90
|
|
90
91
|
CHECKS = Registry[CheckFunction]()
|
91
|
-
|
92
|
+
|
93
|
+
|
94
|
+
def check(func: CheckFunction) -> CheckFunction:
|
95
|
+
"""Register a custom validation check to run against API responses.
|
96
|
+
|
97
|
+
Args:
|
98
|
+
func: Function that takes `(ctx: CheckContext, response: Response, case: Case)` and raises `AssertionError` on validation failure
|
99
|
+
|
100
|
+
Example:
|
101
|
+
```python
|
102
|
+
import schemathesis
|
103
|
+
|
104
|
+
@schemathesis.check
|
105
|
+
def check_cors_headers(ctx, response, case):
|
106
|
+
\"\"\"Verify CORS headers are present\"\"\"
|
107
|
+
if "Access-Control-Allow-Origin" not in response.headers:
|
108
|
+
raise AssertionError("Missing CORS headers")
|
109
|
+
```
|
110
|
+
|
111
|
+
"""
|
112
|
+
return CHECKS.register(func)
|
92
113
|
|
93
114
|
|
94
115
|
@check
|
95
116
|
def not_a_server_error(ctx: CheckContext, response: Response, case: Case) -> bool | None:
|
96
117
|
"""A check to verify that the response is not a server-side error."""
|
97
|
-
from .specs.graphql.schemas import GraphQLSchema
|
98
|
-
from .specs.graphql.validation import validate_graphql_response
|
118
|
+
from schemathesis.specs.graphql.schemas import GraphQLSchema
|
119
|
+
from schemathesis.specs.graphql.validation import validate_graphql_response
|
120
|
+
from schemathesis.specs.openapi.utils import expand_status_codes
|
121
|
+
|
122
|
+
expected_statuses = expand_status_codes(ctx.config.not_a_server_error.expected_statuses or [])
|
99
123
|
|
100
124
|
status_code = response.status_code
|
101
|
-
if status_code
|
125
|
+
if status_code not in expected_statuses:
|
102
126
|
raise ServerError(operation=case.operation.label, status_code=status_code)
|
103
127
|
if isinstance(case.operation.schema, GraphQLSchema):
|
104
128
|
try:
|
@@ -6,7 +6,6 @@ from typing import Any, Callable
|
|
6
6
|
import click
|
7
7
|
from click.utils import LazyFile
|
8
8
|
|
9
|
-
from schemathesis import contrib
|
10
9
|
from schemathesis.checks import CHECKS
|
11
10
|
from schemathesis.cli.commands.run import executor, validation
|
12
11
|
from schemathesis.cli.commands.run.filters import with_filters
|
@@ -19,11 +18,17 @@ from schemathesis.cli.ext.options import (
|
|
19
18
|
CustomHelpMessageChoice,
|
20
19
|
RegistryChoice,
|
21
20
|
)
|
22
|
-
from schemathesis.config import
|
21
|
+
from schemathesis.config import (
|
22
|
+
DEFAULT_REPORT_DIRECTORY,
|
23
|
+
HealthCheck,
|
24
|
+
ReportFormat,
|
25
|
+
SchemathesisConfig,
|
26
|
+
SchemathesisWarning,
|
27
|
+
)
|
23
28
|
from schemathesis.core import HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER
|
24
29
|
from schemathesis.core.transport import DEFAULT_RESPONSE_TIMEOUT
|
25
|
-
from schemathesis.generation import
|
26
|
-
from schemathesis.generation.
|
30
|
+
from schemathesis.generation import GenerationMode
|
31
|
+
from schemathesis.generation.metrics import METRICS, MetricFunction
|
27
32
|
|
28
33
|
# NOTE: Need to explicitly import all registered checks
|
29
34
|
from schemathesis.specs.openapi.checks import * # noqa: F401, F403
|
@@ -83,6 +88,14 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
|
|
83
88
|
default=None,
|
84
89
|
envvar="SCHEMATHESIS_WAIT_FOR_SCHEMA",
|
85
90
|
)
|
91
|
+
@grouped_option(
|
92
|
+
"--warnings",
|
93
|
+
help="Control warning display: 'off' to disable all, or comma-separated list of warning types to enable",
|
94
|
+
type=str,
|
95
|
+
default=None,
|
96
|
+
callback=validation.validate_warnings,
|
97
|
+
metavar="WARNINGS",
|
98
|
+
)
|
86
99
|
@group("API validation options")
|
87
100
|
@grouped_option(
|
88
101
|
"--checks",
|
@@ -296,7 +309,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
|
|
296
309
|
"generation_modes",
|
297
310
|
help="Test data generation mode",
|
298
311
|
type=click.Choice([item.value for item in GenerationMode] + ["all"]),
|
299
|
-
default=
|
312
|
+
default="all",
|
300
313
|
callback=validation.convert_generation_mode,
|
301
314
|
show_default=True,
|
302
315
|
metavar="",
|
@@ -349,7 +362,7 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
|
|
349
362
|
"generation_maximize",
|
350
363
|
multiple=True,
|
351
364
|
help="Guide input generation to values more likely to expose bugs via targeted property-based testing",
|
352
|
-
type=RegistryChoice(
|
365
|
+
type=RegistryChoice(METRICS),
|
353
366
|
default=None,
|
354
367
|
callback=validation.convert_maximize,
|
355
368
|
show_default=True,
|
@@ -390,15 +403,6 @@ DEFAULT_PHASES = ["examples", "coverage", "fuzzing", "stateful"]
|
|
390
403
|
show_default=True,
|
391
404
|
metavar="BOOLEAN",
|
392
405
|
)
|
393
|
-
@grouped_option(
|
394
|
-
"--contrib-openapi-fill-missing-examples",
|
395
|
-
"contrib_openapi_fill_missing_examples",
|
396
|
-
help="Enable generation of random examples for API operations that do not have explicit examples",
|
397
|
-
is_flag=True,
|
398
|
-
default=False,
|
399
|
-
show_default=True,
|
400
|
-
metavar="BOOLEAN",
|
401
|
-
)
|
402
406
|
@group("Global options")
|
403
407
|
@grouped_option("--no-color", help="Disable ANSI color escape codes", type=bool, is_flag=True)
|
404
408
|
@grouped_option("--force-color", help="Explicitly tells to enable ANSI color escape codes", type=bool, is_flag=True)
|
@@ -442,6 +446,7 @@ def run(
|
|
442
446
|
base_url: str | None,
|
443
447
|
wait_for_schema: float | None = None,
|
444
448
|
suppress_health_check: list[HealthCheck] | None,
|
449
|
+
warnings: bool | list[SchemathesisWarning] | None,
|
445
450
|
rate_limit: str | None = None,
|
446
451
|
request_timeout: int | None = None,
|
447
452
|
request_tls_verify: bool = True,
|
@@ -456,11 +461,10 @@ def run(
|
|
456
461
|
report_preserve_bytes: bool = False,
|
457
462
|
output_sanitize: bool = True,
|
458
463
|
output_truncate: bool = True,
|
459
|
-
|
460
|
-
generation_modes: list[GenerationMode] = DEFAULT_GENERATOR_MODES,
|
464
|
+
generation_modes: list[GenerationMode],
|
461
465
|
generation_seed: int | None = None,
|
462
466
|
generation_max_examples: int | None = None,
|
463
|
-
generation_maximize: list[
|
467
|
+
generation_maximize: list[MetricFunction] | None,
|
464
468
|
generation_deterministic: bool = False,
|
465
469
|
generation_database: str | None = None,
|
466
470
|
generation_unique_inputs: bool = False,
|
@@ -494,9 +498,6 @@ def run(
|
|
494
498
|
|
495
499
|
validation.validate_auth_overlap(auth, headers)
|
496
500
|
|
497
|
-
if contrib_openapi_fill_missing_examples:
|
498
|
-
contrib.openapi.fill_missing_examples.install()
|
499
|
-
|
500
501
|
# Then override the global config from CLI options
|
501
502
|
config.update(
|
502
503
|
color=color,
|
@@ -528,6 +529,7 @@ def run(
|
|
528
529
|
request_cert=request_cert,
|
529
530
|
request_cert_key=request_cert_key,
|
530
531
|
proxy=request_proxy,
|
532
|
+
warnings=warnings,
|
531
533
|
)
|
532
534
|
# These are filters for what API operations should be tested
|
533
535
|
filter_set = {
|
@@ -1,8 +1,9 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
from dataclasses import dataclass, field
|
4
|
-
from typing import TYPE_CHECKING, Generator
|
4
|
+
from typing import TYPE_CHECKING, Callable, Generator
|
5
5
|
|
6
|
+
from schemathesis.cli.commands.run.events import LoadingFinished
|
6
7
|
from schemathesis.config import ProjectConfig
|
7
8
|
from schemathesis.core.failures import Failure
|
8
9
|
from schemathesis.core.result import Err, Ok
|
@@ -11,6 +12,7 @@ from schemathesis.core.transport import Response
|
|
11
12
|
from schemathesis.engine import Status, events
|
12
13
|
from schemathesis.engine.recorder import CaseNode, ScenarioRecorder
|
13
14
|
from schemathesis.generation.case import Case
|
15
|
+
from schemathesis.schemas import APIOperation
|
14
16
|
|
15
17
|
if TYPE_CHECKING:
|
16
18
|
from schemathesis.generation.stateful.state_machine import ExtractionFailure
|
@@ -177,6 +179,7 @@ class ExecutionContext:
|
|
177
179
|
"""Storage for the current context of the execution."""
|
178
180
|
|
179
181
|
config: ProjectConfig
|
182
|
+
find_operation_by_label: Callable[[str], APIOperation | None] | None = None
|
180
183
|
statistic: Statistic = field(default_factory=Statistic)
|
181
184
|
exit_code: int = 0
|
182
185
|
initialization_lines: list[str | Generator[str, None, None]] = field(default_factory=list)
|
@@ -189,6 +192,8 @@ class ExecutionContext:
|
|
189
192
|
self.summary_lines.append(line)
|
190
193
|
|
191
194
|
def on_event(self, event: events.EngineEvent) -> None:
|
195
|
+
if isinstance(event, LoadingFinished):
|
196
|
+
self.find_operation_by_label = event.find_operation_by_label
|
192
197
|
if isinstance(event, events.ScenarioFinished):
|
193
198
|
self.statistic.on_scenario_finished(event.recorder)
|
194
199
|
elif isinstance(event, events.NonFatalError) or (
|
@@ -1,10 +1,13 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import time
|
2
4
|
import uuid
|
5
|
+
from typing import Callable
|
3
6
|
|
4
7
|
from schemathesis.config import ProjectConfig
|
5
8
|
from schemathesis.core import Specification
|
6
9
|
from schemathesis.engine import events
|
7
|
-
from schemathesis.schemas import ApiStatistic
|
10
|
+
from schemathesis.schemas import APIOperation, ApiStatistic
|
8
11
|
|
9
12
|
|
10
13
|
class LoadingStarted(events.EngineEvent):
|
@@ -28,6 +31,7 @@ class LoadingFinished(events.EngineEvent):
|
|
28
31
|
"statistic",
|
29
32
|
"schema",
|
30
33
|
"config",
|
34
|
+
"find_operation_by_label",
|
31
35
|
)
|
32
36
|
|
33
37
|
def __init__(
|
@@ -41,6 +45,7 @@ class LoadingFinished(events.EngineEvent):
|
|
41
45
|
statistic: ApiStatistic,
|
42
46
|
schema: dict,
|
43
47
|
config: ProjectConfig,
|
48
|
+
find_operation_by_label: Callable[[str], APIOperation | None],
|
44
49
|
) -> None:
|
45
50
|
self.id = uuid.uuid4()
|
46
51
|
self.timestamp = time.time()
|
@@ -52,3 +57,4 @@ class LoadingFinished(events.EngineEvent):
|
|
52
57
|
self.schema = schema
|
53
58
|
self.base_path = base_path
|
54
59
|
self.config = config
|
60
|
+
self.find_operation_by_label = find_operation_by_label
|
@@ -76,6 +76,7 @@ def into_event_stream(*, location: str, config: ProjectConfig, filter_set: dict[
|
|
76
76
|
schema=schema.raw_schema,
|
77
77
|
config=schema.config,
|
78
78
|
base_path=schema.base_path,
|
79
|
+
find_operation_by_label=schema.find_operation_by_label,
|
79
80
|
)
|
80
81
|
|
81
82
|
try:
|
@@ -121,17 +122,21 @@ def _execute(
|
|
121
122
|
args: list[str],
|
122
123
|
params: dict[str, Any],
|
123
124
|
) -> None:
|
124
|
-
handlers
|
125
|
-
ctx =
|
125
|
+
handlers: list[EventHandler] = []
|
126
|
+
ctx: ExecutionContext | None = None
|
126
127
|
|
127
128
|
def shutdown() -> None:
|
128
|
-
|
129
|
-
_handler
|
130
|
-
|
131
|
-
for handler in handlers:
|
132
|
-
handler.start(ctx)
|
129
|
+
if ctx is not None:
|
130
|
+
for _handler in handlers:
|
131
|
+
_handler.shutdown(ctx)
|
133
132
|
|
134
133
|
try:
|
134
|
+
handlers = initialize_handlers(config=config, args=args, params=params)
|
135
|
+
ctx = ExecutionContext(config=config)
|
136
|
+
|
137
|
+
for handler in handlers:
|
138
|
+
handler.start(ctx)
|
139
|
+
|
135
140
|
for event in event_stream:
|
136
141
|
ctx.on_event(event)
|
137
142
|
for handler in handlers:
|