schemathesis 3.25.6__py3-none-any.whl → 3.39.7__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 +6 -6
- schemathesis/_compat.py +2 -2
- schemathesis/_dependency_versions.py +4 -2
- schemathesis/_hypothesis.py +369 -56
- schemathesis/_lazy_import.py +1 -0
- schemathesis/_override.py +5 -4
- schemathesis/_patches.py +21 -0
- schemathesis/_rate_limiter.py +7 -0
- schemathesis/_xml.py +75 -22
- schemathesis/auths.py +78 -16
- schemathesis/checks.py +21 -9
- schemathesis/cli/__init__.py +783 -432
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/callbacks.py +58 -13
- schemathesis/cli/cassettes.py +233 -47
- schemathesis/cli/constants.py +8 -2
- schemathesis/cli/context.py +22 -5
- schemathesis/cli/debug.py +2 -1
- schemathesis/cli/handlers.py +4 -1
- schemathesis/cli/junitxml.py +103 -22
- schemathesis/cli/options.py +15 -4
- schemathesis/cli/output/default.py +258 -112
- schemathesis/cli/output/short.py +23 -8
- schemathesis/cli/reporting.py +79 -0
- schemathesis/cli/sanitization.py +6 -0
- schemathesis/code_samples.py +5 -3
- schemathesis/constants.py +1 -0
- schemathesis/contrib/openapi/__init__.py +1 -1
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
- schemathesis/contrib/openapi/formats/uuid.py +2 -1
- schemathesis/contrib/unique_data.py +3 -3
- schemathesis/exceptions.py +76 -65
- schemathesis/experimental/__init__.py +35 -0
- schemathesis/extra/_aiohttp.py +1 -0
- schemathesis/extra/_flask.py +4 -1
- schemathesis/extra/_server.py +1 -0
- schemathesis/extra/pytest_plugin.py +17 -25
- schemathesis/failures.py +77 -9
- schemathesis/filters.py +185 -8
- schemathesis/fixups/__init__.py +1 -0
- schemathesis/fixups/fast_api.py +2 -2
- schemathesis/fixups/utf8_bom.py +1 -2
- schemathesis/generation/__init__.py +20 -36
- schemathesis/generation/_hypothesis.py +59 -0
- schemathesis/generation/_methods.py +44 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/graphql.py +0 -1
- schemathesis/hooks.py +89 -12
- schemathesis/internal/checks.py +84 -0
- schemathesis/internal/copy.py +22 -3
- schemathesis/internal/deprecation.py +6 -2
- schemathesis/internal/diff.py +15 -0
- schemathesis/internal/extensions.py +27 -0
- schemathesis/internal/jsonschema.py +2 -1
- schemathesis/internal/output.py +68 -0
- schemathesis/internal/result.py +1 -1
- schemathesis/internal/transformation.py +11 -0
- schemathesis/lazy.py +138 -25
- schemathesis/loaders.py +7 -5
- schemathesis/models.py +318 -211
- schemathesis/parameters.py +4 -0
- schemathesis/runner/__init__.py +50 -15
- schemathesis/runner/events.py +65 -5
- schemathesis/runner/impl/context.py +104 -0
- schemathesis/runner/impl/core.py +388 -177
- schemathesis/runner/impl/solo.py +19 -29
- schemathesis/runner/impl/threadpool.py +70 -79
- schemathesis/runner/probes.py +11 -9
- schemathesis/runner/serialization.py +150 -17
- schemathesis/sanitization.py +5 -1
- schemathesis/schemas.py +170 -102
- schemathesis/serializers.py +7 -2
- schemathesis/service/ci.py +1 -0
- schemathesis/service/client.py +39 -6
- schemathesis/service/events.py +5 -1
- schemathesis/service/extensions.py +224 -0
- schemathesis/service/hosts.py +6 -2
- schemathesis/service/metadata.py +25 -0
- schemathesis/service/models.py +211 -2
- schemathesis/service/report.py +6 -6
- schemathesis/service/serialization.py +45 -71
- schemathesis/service/usage.py +1 -0
- schemathesis/specs/graphql/_cache.py +26 -0
- schemathesis/specs/graphql/loaders.py +25 -5
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +2 -2
- schemathesis/specs/graphql/schemas.py +130 -100
- schemathesis/specs/graphql/validation.py +1 -2
- schemathesis/specs/openapi/__init__.py +1 -0
- schemathesis/specs/openapi/_cache.py +123 -0
- schemathesis/specs/openapi/_hypothesis.py +78 -60
- schemathesis/specs/openapi/checks.py +504 -25
- schemathesis/specs/openapi/converter.py +31 -4
- schemathesis/specs/openapi/definitions.py +10 -17
- schemathesis/specs/openapi/examples.py +126 -12
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +1 -1
- schemathesis/specs/openapi/expressions/extractors.py +26 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +29 -6
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +44 -0
- schemathesis/specs/openapi/links.py +125 -42
- schemathesis/specs/openapi/loaders.py +77 -36
- schemathesis/specs/openapi/media_types.py +34 -0
- schemathesis/specs/openapi/negative/__init__.py +6 -3
- schemathesis/specs/openapi/negative/mutations.py +21 -6
- schemathesis/specs/openapi/parameters.py +39 -25
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +37 -7
- schemathesis/specs/openapi/schemas.py +360 -241
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +1 -0
- schemathesis/specs/openapi/stateful/__init__.py +198 -70
- schemathesis/specs/openapi/stateful/statistic.py +198 -0
- schemathesis/specs/openapi/stateful/types.py +14 -0
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/specs/openapi/validation.py +1 -0
- schemathesis/stateful/__init__.py +35 -21
- schemathesis/stateful/config.py +97 -0
- schemathesis/stateful/context.py +135 -0
- schemathesis/stateful/events.py +274 -0
- schemathesis/stateful/runner.py +309 -0
- schemathesis/stateful/sink.py +68 -0
- schemathesis/stateful/state_machine.py +67 -38
- schemathesis/stateful/statistic.py +22 -0
- schemathesis/stateful/validation.py +100 -0
- schemathesis/targets.py +33 -1
- schemathesis/throttling.py +25 -5
- schemathesis/transports/__init__.py +354 -0
- schemathesis/transports/asgi.py +7 -0
- schemathesis/transports/auth.py +25 -2
- schemathesis/transports/content_types.py +3 -1
- schemathesis/transports/headers.py +2 -1
- schemathesis/transports/responses.py +9 -4
- schemathesis/types.py +9 -0
- schemathesis/utils.py +11 -16
- schemathesis-3.39.7.dist-info/METADATA +293 -0
- schemathesis-3.39.7.dist-info/RECORD +160 -0
- {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis-3.25.6.dist-info/METADATA +0 -356
- schemathesis-3.25.6.dist-info/RECORD +0 -134
- {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
schemathesis/cli/__init__.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import base64
|
|
3
|
-
import enum
|
|
4
|
-
import io
|
|
5
4
|
import os
|
|
6
5
|
import sys
|
|
7
6
|
import traceback
|
|
@@ -10,66 +9,77 @@ from collections import defaultdict
|
|
|
10
9
|
from dataclasses import dataclass
|
|
11
10
|
from enum import Enum
|
|
12
11
|
from queue import Queue
|
|
13
|
-
from typing import Any, Callable, Generator, Iterable, NoReturn,
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, Literal, NoReturn, Sequence, Type, cast
|
|
14
13
|
from urllib.parse import urlparse
|
|
15
14
|
|
|
16
15
|
import click
|
|
17
16
|
|
|
18
17
|
from .. import checks as checks_module
|
|
19
|
-
from .. import contrib, experimental, generation
|
|
18
|
+
from .. import contrib, experimental, generation, runner, service
|
|
20
19
|
from .. import fixups as _fixups
|
|
21
|
-
from .. import runner, service
|
|
22
20
|
from .. import targets as targets_module
|
|
21
|
+
from .._override import CaseOverride
|
|
23
22
|
from ..code_samples import CodeSampleStyle
|
|
24
|
-
from .constants import HealthCheck, Phase, Verbosity
|
|
25
|
-
from ..generation import DEFAULT_DATA_GENERATION_METHODS, DataGenerationMethod
|
|
26
23
|
from ..constants import (
|
|
27
24
|
API_NAME_ENV_VAR,
|
|
28
25
|
BASE_URL_ENV_VAR,
|
|
29
26
|
DEFAULT_RESPONSE_TIMEOUT,
|
|
30
27
|
DEFAULT_STATEFUL_RECURSION_LIMIT,
|
|
28
|
+
EXTENSIONS_DOCUMENTATION_URL,
|
|
31
29
|
HOOKS_MODULE_ENV_VAR,
|
|
32
30
|
HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER,
|
|
33
|
-
WAIT_FOR_SCHEMA_ENV_VAR,
|
|
34
|
-
EXTENSIONS_DOCUMENTATION_URL,
|
|
35
31
|
ISSUE_TRACKER_URL,
|
|
32
|
+
WAIT_FOR_SCHEMA_ENV_VAR,
|
|
36
33
|
)
|
|
37
|
-
from ..exceptions import SchemaError,
|
|
34
|
+
from ..exceptions import SchemaError, SchemaErrorType, extract_nth_traceback
|
|
35
|
+
from ..filters import FilterSet, expression_to_filter_function, is_deprecated
|
|
38
36
|
from ..fixups import ALL_FIXUPS
|
|
39
|
-
from ..
|
|
40
|
-
from .._override import CaseOverride
|
|
41
|
-
from ..transports.auth import get_requests_auth
|
|
37
|
+
from ..generation import DEFAULT_DATA_GENERATION_METHODS, DataGenerationMethod
|
|
42
38
|
from ..hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookScope
|
|
43
|
-
from ..
|
|
39
|
+
from ..internal.checks import CheckConfig
|
|
40
|
+
from ..internal.datetime import current_datetime
|
|
41
|
+
from ..internal.output import OutputConfig
|
|
42
|
+
from ..internal.validation import file_exists
|
|
43
|
+
from ..loaders import load_app, load_yaml
|
|
44
44
|
from ..runner import events, prepare_hypothesis_settings, probes
|
|
45
45
|
from ..specs.graphql import loaders as gql_loaders
|
|
46
46
|
from ..specs.openapi import loaders as oas_loaders
|
|
47
47
|
from ..stateful import Stateful
|
|
48
|
-
from ..
|
|
49
|
-
from ..
|
|
50
|
-
from ..internal.datetime import current_datetime
|
|
51
|
-
from ..internal.validation import file_exists
|
|
48
|
+
from ..transports import RequestConfig
|
|
49
|
+
from ..transports.auth import get_requests_auth
|
|
52
50
|
from . import callbacks, cassettes, output
|
|
53
|
-
from .constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS
|
|
51
|
+
from .constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS, HealthCheck, Phase, Verbosity
|
|
54
52
|
from .context import ExecutionContext, FileReportContext, ServiceReportContext
|
|
55
53
|
from .debug import DebugOutputHandler
|
|
54
|
+
from .handlers import EventHandler
|
|
56
55
|
from .junitxml import JunitXMLHandler
|
|
57
|
-
from .options import CsvChoice, CsvEnumChoice,
|
|
56
|
+
from .options import CsvChoice, CsvEnumChoice, CsvListChoice, CustomHelpMessageChoice, OptionalInt
|
|
58
57
|
from .sanitization import SanitizationHandler
|
|
59
58
|
|
|
60
59
|
if TYPE_CHECKING:
|
|
60
|
+
import io
|
|
61
|
+
|
|
61
62
|
import hypothesis
|
|
62
63
|
import requests
|
|
63
|
-
|
|
64
|
+
|
|
65
|
+
from ..models import Case, CheckFunction
|
|
64
66
|
from ..schemas import BaseSchema
|
|
67
|
+
from ..service.client import ServiceClient
|
|
65
68
|
from ..specs.graphql.schemas import GraphQLSchema
|
|
66
|
-
from
|
|
69
|
+
from ..targets import Target
|
|
70
|
+
from ..types import NotSet, PathLike, RequestCert
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
__all__ = [
|
|
74
|
+
"EventHandler",
|
|
75
|
+
]
|
|
67
76
|
|
|
68
77
|
|
|
69
78
|
def _get_callable_names(items: tuple[Callable, ...]) -> tuple[str, ...]:
|
|
70
79
|
return tuple(item.__name__ for item in items)
|
|
71
80
|
|
|
72
81
|
|
|
82
|
+
CUSTOM_HANDLERS: list[type[EventHandler]] = []
|
|
73
83
|
CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
|
|
74
84
|
|
|
75
85
|
DEFAULT_CHECKS_NAMES = _get_callable_names(checks_module.DEFAULT_CHECKS)
|
|
@@ -95,13 +105,6 @@ DEPRECATED_SHOW_ERROR_TRACEBACKS_OPTION_WARNING = (
|
|
|
95
105
|
"Warning: Option `--show-errors-tracebacks` is deprecated and will be removed in Schemathesis 4.0. "
|
|
96
106
|
"Use `--show-trace` instead"
|
|
97
107
|
)
|
|
98
|
-
DEPRECATED_CONTRIB_UNIQUE_DATA_OPTION_WARNING = (
|
|
99
|
-
"The `--contrib-unique-data` CLI option and the corresponding `schemathesis.contrib.unique_data` hook "
|
|
100
|
-
"are **DEPRECATED**. The concept of this feature does not fit the core principles of Hypothesis where "
|
|
101
|
-
"strategies are configurable on a per-example basis but this feature implies uniqueness across examples. "
|
|
102
|
-
"This leads to cryptic error messages about external state and flaky test runs, "
|
|
103
|
-
"therefore it will be removed in Schemathesis 4.0"
|
|
104
|
-
)
|
|
105
108
|
CASSETTES_PATH_INVALID_USAGE_MESSAGE = "Can't use `--store-network-log` and `--cassette-path` simultaneously"
|
|
106
109
|
COLOR_OPTIONS_INVALID_USAGE_MESSAGE = "Can't use `--no-color` and `--force-color` simultaneously"
|
|
107
110
|
PHASES_INVALID_USAGE_MESSAGE = "Can't use `--hypothesis-phases` and `--hypothesis-no-phases` simultaneously"
|
|
@@ -111,21 +114,21 @@ def reset_checks() -> None:
|
|
|
111
114
|
"""Get checks list to their default state."""
|
|
112
115
|
# Useful in tests
|
|
113
116
|
checks_module.ALL_CHECKS = checks_module.DEFAULT_CHECKS + checks_module.OPTIONAL_CHECKS
|
|
114
|
-
CHECKS_TYPE.choices = _get_callable_names(checks_module.ALL_CHECKS)
|
|
117
|
+
CHECKS_TYPE.choices = (*_get_callable_names(checks_module.ALL_CHECKS), "all")
|
|
115
118
|
|
|
116
119
|
|
|
117
120
|
def reset_targets() -> None:
|
|
118
121
|
"""Get targets list to their default state."""
|
|
119
122
|
# Useful in tests
|
|
120
123
|
targets_module.ALL_TARGETS = targets_module.DEFAULT_TARGETS + targets_module.OPTIONAL_TARGETS
|
|
121
|
-
TARGETS_TYPE.choices = _get_callable_names(targets_module.ALL_TARGETS)
|
|
124
|
+
TARGETS_TYPE.choices = (*_get_callable_names(targets_module.ALL_TARGETS), "all")
|
|
122
125
|
|
|
123
126
|
|
|
124
127
|
@click.group(context_settings=CONTEXT_SETTINGS)
|
|
125
|
-
@click.option("--pre-run", help="A module to execute before running the tests
|
|
128
|
+
@click.option("--pre-run", help="[DEPRECATED] A module to execute before running the tests", type=str, hidden=True)
|
|
126
129
|
@click.version_option()
|
|
127
130
|
def schemathesis(pre_run: str | None = None) -> None:
|
|
128
|
-
"""
|
|
131
|
+
"""Property-based API testing for OpenAPI and GraphQL."""
|
|
129
132
|
# Don't use `envvar=HOOKS_MODULE_ENV_VAR` arg to raise a deprecation warning for hooks
|
|
130
133
|
hooks: str | None
|
|
131
134
|
if pre_run:
|
|
@@ -137,76 +140,89 @@ def schemathesis(pre_run: str | None = None) -> None:
|
|
|
137
140
|
load_hook(hooks)
|
|
138
141
|
|
|
139
142
|
|
|
140
|
-
|
|
141
|
-
filtering = "Testing scope", "Customize the scope of the API testing."
|
|
142
|
-
validation = "Response & Schema validation", "These options specify how API responses and schemas are validated."
|
|
143
|
-
hypothesis = "Hypothesis engine", "Configuration of the underlying Hypothesis engine."
|
|
144
|
-
generic = "Generic", None
|
|
143
|
+
GROUPS: list[str] = []
|
|
145
144
|
|
|
146
145
|
|
|
147
|
-
class
|
|
146
|
+
class CommandWithGroupedOptions(click.Command):
|
|
148
147
|
def format_options(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
|
149
|
-
# Group options first
|
|
150
148
|
groups = defaultdict(list)
|
|
151
149
|
for param in self.get_params(ctx):
|
|
152
150
|
rv = param.get_help_record(ctx)
|
|
153
151
|
if rv is not None:
|
|
152
|
+
(option_repr, message) = rv
|
|
153
|
+
if isinstance(param.type, click.Choice):
|
|
154
|
+
message += (
|
|
155
|
+
getattr(param.type, "choices_repr", None)
|
|
156
|
+
or f" [possible values: {', '.join(param.type.choices)}]"
|
|
157
|
+
)
|
|
158
|
+
|
|
154
159
|
if isinstance(param, GroupedOption):
|
|
155
160
|
group = param.group
|
|
156
161
|
else:
|
|
157
|
-
group =
|
|
158
|
-
groups[group].append(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
title, description = group.value
|
|
163
|
-
with formatter.section(title):
|
|
164
|
-
if description:
|
|
165
|
-
formatter.write_paragraph()
|
|
166
|
-
formatter.write_text(description)
|
|
167
|
-
formatter.write_paragraph()
|
|
168
|
-
formatter.write_dl(opts)
|
|
162
|
+
group = "Global options"
|
|
163
|
+
groups[group].append((option_repr, message))
|
|
164
|
+
for group in GROUPS:
|
|
165
|
+
with formatter.section(group or "Options"):
|
|
166
|
+
formatter.write_dl(groups[group], col_max=40)
|
|
169
167
|
|
|
170
168
|
|
|
171
169
|
class GroupedOption(click.Option):
|
|
172
|
-
def __init__(self, *args: Any, group:
|
|
170
|
+
def __init__(self, *args: Any, group: str | None = None, **kwargs: Any):
|
|
173
171
|
super().__init__(*args, **kwargs)
|
|
174
172
|
self.group = group
|
|
175
173
|
|
|
176
174
|
|
|
177
|
-
|
|
175
|
+
def group(name: str) -> Callable:
|
|
176
|
+
GROUPS.append(name)
|
|
177
|
+
|
|
178
|
+
def _inner(cmd: Callable) -> Callable:
|
|
179
|
+
for param in reversed(cmd.__click_params__): # type: ignore[attr-defined]
|
|
180
|
+
if not isinstance(param, GroupedOption) or param.group is not None:
|
|
181
|
+
break
|
|
182
|
+
param.group = name
|
|
183
|
+
return cmd
|
|
184
|
+
|
|
185
|
+
return _inner
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def grouped_option(*args: Any, **kwargs: Any) -> Callable:
|
|
189
|
+
kwargs.setdefault("cls", GroupedOption)
|
|
190
|
+
return click.option(*args, **kwargs)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
with_request_proxy = grouped_option(
|
|
178
194
|
"--request-proxy",
|
|
179
|
-
help="Set the proxy for all network requests
|
|
195
|
+
help="Set the proxy for all network requests",
|
|
180
196
|
type=str,
|
|
181
197
|
)
|
|
182
|
-
with_request_tls_verify =
|
|
198
|
+
with_request_tls_verify = grouped_option(
|
|
183
199
|
"--request-tls-verify",
|
|
184
|
-
help="Configures TLS certificate verification for server requests. Can specify path to CA_BUNDLE for custom certs
|
|
200
|
+
help="Configures TLS certificate verification for server requests. Can specify path to CA_BUNDLE for custom certs",
|
|
185
201
|
type=str,
|
|
186
202
|
default="true",
|
|
187
203
|
show_default=True,
|
|
188
204
|
callback=callbacks.convert_boolean_string,
|
|
189
205
|
)
|
|
190
|
-
with_request_cert =
|
|
206
|
+
with_request_cert = grouped_option(
|
|
191
207
|
"--request-cert",
|
|
192
208
|
help="File path of unencrypted client certificate for authentication. "
|
|
193
209
|
"The certificate can be bundled with a private key (e.g. PEM) or the private "
|
|
194
|
-
"key can be provided with the --request-cert-key argument
|
|
210
|
+
"key can be provided with the --request-cert-key argument",
|
|
195
211
|
type=click.Path(exists=True),
|
|
196
212
|
default=None,
|
|
197
213
|
show_default=False,
|
|
198
214
|
)
|
|
199
|
-
with_request_cert_key =
|
|
215
|
+
with_request_cert_key = grouped_option(
|
|
200
216
|
"--request-cert-key",
|
|
201
|
-
help="
|
|
217
|
+
help="Specify the file path of the private key for the client certificate",
|
|
202
218
|
type=click.Path(exists=True),
|
|
203
219
|
default=None,
|
|
204
220
|
show_default=False,
|
|
205
221
|
callback=callbacks.validate_request_cert_key,
|
|
206
222
|
)
|
|
207
|
-
with_hosts_file =
|
|
223
|
+
with_hosts_file = grouped_option(
|
|
208
224
|
"--hosts-file",
|
|
209
|
-
help="Path to a file to store the Schemathesis.io auth configuration
|
|
225
|
+
help="Path to a file to store the Schemathesis.io auth configuration",
|
|
210
226
|
type=click.Path(dir_okay=False, writable=True),
|
|
211
227
|
default=service.DEFAULT_HOSTS_PATH,
|
|
212
228
|
envvar=service.HOSTS_PATH_ENV_VAR,
|
|
@@ -214,6 +230,37 @@ with_hosts_file = click.option(
|
|
|
214
230
|
)
|
|
215
231
|
|
|
216
232
|
|
|
233
|
+
def _with_filter(*, by: str, mode: Literal["include", "exclude"], modifier: Literal["regex"] | None = None) -> Callable:
|
|
234
|
+
"""Generate a CLI option for filtering API operations."""
|
|
235
|
+
param = f"--{mode}-{by}"
|
|
236
|
+
action = "include in" if mode == "include" else "exclude from"
|
|
237
|
+
prop = {
|
|
238
|
+
"operation-id": "ID",
|
|
239
|
+
"name": "Operation name",
|
|
240
|
+
}.get(by, by.capitalize())
|
|
241
|
+
if modifier:
|
|
242
|
+
param += f"-{modifier}"
|
|
243
|
+
prop += " pattern"
|
|
244
|
+
help_text = f"{prop} to {action} testing."
|
|
245
|
+
return grouped_option(
|
|
246
|
+
param,
|
|
247
|
+
help=help_text,
|
|
248
|
+
type=str,
|
|
249
|
+
multiple=modifier is None,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
_BY_VALUES = ("operation-id", "tag", "name", "method", "path")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def with_filters(command: Callable) -> Callable:
|
|
257
|
+
for by in _BY_VALUES:
|
|
258
|
+
for mode in ("exclude", "include"):
|
|
259
|
+
for modifier in ("regex", None):
|
|
260
|
+
command = _with_filter(by=by, mode=mode, modifier=modifier)(command) # type: ignore[arg-type]
|
|
261
|
+
return command
|
|
262
|
+
|
|
263
|
+
|
|
217
264
|
class ReportToService:
|
|
218
265
|
pass
|
|
219
266
|
|
|
@@ -221,502 +268,587 @@ class ReportToService:
|
|
|
221
268
|
REPORT_TO_SERVICE = ReportToService()
|
|
222
269
|
|
|
223
270
|
|
|
224
|
-
@schemathesis.command(
|
|
271
|
+
@schemathesis.command(
|
|
272
|
+
short_help="Execute automated tests based on API specifications",
|
|
273
|
+
cls=CommandWithGroupedOptions,
|
|
274
|
+
context_settings={"terminal_width": output.default.get_terminal_width(), **CONTEXT_SETTINGS},
|
|
275
|
+
)
|
|
225
276
|
@click.argument("schema", type=str)
|
|
226
277
|
@click.argument("api_name", type=str, required=False, envvar=API_NAME_ENV_VAR)
|
|
227
|
-
@
|
|
278
|
+
@group("Options")
|
|
279
|
+
@grouped_option(
|
|
280
|
+
"--workers",
|
|
281
|
+
"-w",
|
|
282
|
+
"workers_num",
|
|
283
|
+
help="Number of concurrent workers for testing. Auto-adjusts if 'auto' is specified",
|
|
284
|
+
type=CustomHelpMessageChoice(
|
|
285
|
+
["auto", *list(map(str, range(MIN_WORKERS, MAX_WORKERS + 1)))],
|
|
286
|
+
choices_repr=f"[auto, {MIN_WORKERS}-{MAX_WORKERS}]",
|
|
287
|
+
),
|
|
288
|
+
default=str(DEFAULT_WORKERS),
|
|
289
|
+
show_default=True,
|
|
290
|
+
callback=callbacks.convert_workers,
|
|
291
|
+
metavar="",
|
|
292
|
+
)
|
|
293
|
+
@grouped_option(
|
|
294
|
+
"--dry-run",
|
|
295
|
+
"dry_run",
|
|
296
|
+
is_flag=True,
|
|
297
|
+
default=False,
|
|
298
|
+
help="Simulate test execution without making any actual requests, useful for validating data generation",
|
|
299
|
+
)
|
|
300
|
+
@grouped_option(
|
|
301
|
+
"--fixups",
|
|
302
|
+
help="Apply compatibility adjustments",
|
|
303
|
+
multiple=True,
|
|
304
|
+
type=click.Choice([*ALL_FIXUPS, "all"]),
|
|
305
|
+
metavar="",
|
|
306
|
+
)
|
|
307
|
+
@group("Experimental options")
|
|
308
|
+
@grouped_option(
|
|
309
|
+
"--experimental",
|
|
310
|
+
"experiments",
|
|
311
|
+
help="Enable experimental features",
|
|
312
|
+
type=click.Choice(
|
|
313
|
+
[
|
|
314
|
+
experimental.OPEN_API_3_1.name,
|
|
315
|
+
experimental.SCHEMA_ANALYSIS.name,
|
|
316
|
+
experimental.STATEFUL_TEST_RUNNER.name,
|
|
317
|
+
experimental.STATEFUL_ONLY.name,
|
|
318
|
+
experimental.COVERAGE_PHASE.name,
|
|
319
|
+
experimental.POSITIVE_DATA_ACCEPTANCE.name,
|
|
320
|
+
]
|
|
321
|
+
),
|
|
322
|
+
callback=callbacks.convert_experimental,
|
|
323
|
+
multiple=True,
|
|
324
|
+
metavar="",
|
|
325
|
+
)
|
|
326
|
+
@grouped_option(
|
|
327
|
+
"--experimental-no-failfast",
|
|
328
|
+
"no_failfast",
|
|
329
|
+
help="Continue testing an API operation after a failure is found",
|
|
330
|
+
is_flag=True,
|
|
331
|
+
default=False,
|
|
332
|
+
metavar="",
|
|
333
|
+
envvar="SCHEMATHESIS_EXPERIMENTAL_NO_FAILFAST",
|
|
334
|
+
)
|
|
335
|
+
@grouped_option(
|
|
336
|
+
"--experimental-missing-required-header-allowed-statuses",
|
|
337
|
+
"missing_required_header_allowed_statuses",
|
|
338
|
+
help="Comma-separated list of status codes expected for test cases with a missing required header",
|
|
339
|
+
type=CsvListChoice(),
|
|
340
|
+
callback=callbacks.convert_status_codes,
|
|
341
|
+
metavar="",
|
|
342
|
+
envvar="SCHEMATHESIS_EXPERIMENTAL_MISSING_REQUIRED_HEADER_ALLOWED_STATUSES",
|
|
343
|
+
)
|
|
344
|
+
@grouped_option(
|
|
345
|
+
"--experimental-positive-data-acceptance-allowed-statuses",
|
|
346
|
+
"positive_data_acceptance_allowed_statuses",
|
|
347
|
+
help="Comma-separated list of status codes considered as successful responses",
|
|
348
|
+
type=CsvListChoice(),
|
|
349
|
+
callback=callbacks.convert_status_codes,
|
|
350
|
+
metavar="",
|
|
351
|
+
envvar="SCHEMATHESIS_EXPERIMENTAL_POSITIVE_DATA_ACCEPTANCE_ALLOWED_STATUSES",
|
|
352
|
+
)
|
|
353
|
+
@grouped_option(
|
|
354
|
+
"--experimental-negative-data-rejection-allowed-statuses",
|
|
355
|
+
"negative_data_rejection_allowed_statuses",
|
|
356
|
+
help="Comma-separated list of status codes expected for rejected negative data",
|
|
357
|
+
type=CsvListChoice(),
|
|
358
|
+
callback=callbacks.convert_status_codes,
|
|
359
|
+
metavar="",
|
|
360
|
+
envvar="SCHEMATHESIS_EXPERIMENTAL_NEGATIVE_DATA_REJECTION_ALLOWED_STATUSES",
|
|
361
|
+
)
|
|
362
|
+
@group("API validation options")
|
|
363
|
+
@grouped_option(
|
|
228
364
|
"--checks",
|
|
229
365
|
"-c",
|
|
230
366
|
multiple=True,
|
|
231
|
-
help="
|
|
232
|
-
"Provide a comma-separated list of checks such as 'not_a_server_error,status_code_conformance', etc. "
|
|
233
|
-
f"Default is '{','.join(DEFAULT_CHECKS_NAMES)}'.",
|
|
367
|
+
help="Comma-separated list of checks to run against API responses",
|
|
234
368
|
type=CHECKS_TYPE,
|
|
235
369
|
default=DEFAULT_CHECKS_NAMES,
|
|
236
|
-
cls=GroupedOption,
|
|
237
|
-
group=ParameterGroup.validation,
|
|
238
370
|
callback=callbacks.convert_checks,
|
|
239
371
|
show_default=True,
|
|
372
|
+
metavar="",
|
|
240
373
|
)
|
|
241
|
-
@
|
|
374
|
+
@grouped_option(
|
|
242
375
|
"--exclude-checks",
|
|
243
376
|
multiple=True,
|
|
244
|
-
help="
|
|
245
|
-
"Provide a comma-separated list of checks you wish to bypass.",
|
|
377
|
+
help="Comma-separated list of checks to skip during testing",
|
|
246
378
|
type=EXCLUDE_CHECKS_TYPE,
|
|
247
379
|
default=[],
|
|
248
|
-
cls=GroupedOption,
|
|
249
|
-
group=ParameterGroup.validation,
|
|
250
380
|
callback=callbacks.convert_checks,
|
|
251
381
|
show_default=True,
|
|
382
|
+
metavar="",
|
|
252
383
|
)
|
|
253
|
-
@
|
|
254
|
-
"--data-generation-method",
|
|
255
|
-
"-D",
|
|
256
|
-
"data_generation_methods",
|
|
257
|
-
help="Specifies the approach Schemathesis uses to generate test data. "
|
|
258
|
-
"Use 'positive' for valid data, 'negative' for invalid data, or 'all' for both. "
|
|
259
|
-
"Default is 'positive'.",
|
|
260
|
-
type=DATA_GENERATION_METHOD_TYPE,
|
|
261
|
-
default=DataGenerationMethod.default().name,
|
|
262
|
-
callback=callbacks.convert_data_generation_method,
|
|
263
|
-
show_default=True,
|
|
264
|
-
)
|
|
265
|
-
@click.option(
|
|
384
|
+
@grouped_option(
|
|
266
385
|
"--max-response-time",
|
|
267
|
-
help="
|
|
268
|
-
"The test will fail if a response time exceeds this limit. "
|
|
269
|
-
"Provide the time in milliseconds.",
|
|
386
|
+
help="Time limit in milliseconds for API response times. "
|
|
387
|
+
"The test will fail if a response time exceeds this limit. ",
|
|
270
388
|
type=click.IntRange(min=1),
|
|
271
|
-
cls=GroupedOption,
|
|
272
|
-
group=ParameterGroup.validation,
|
|
273
389
|
)
|
|
274
|
-
@
|
|
275
|
-
"--target",
|
|
276
|
-
"-t",
|
|
277
|
-
"targets",
|
|
278
|
-
multiple=True,
|
|
279
|
-
help="Guides input generation to values more likely to expose bugs via targeted property-based testing.",
|
|
280
|
-
type=TARGETS_TYPE,
|
|
281
|
-
default=DEFAULT_TARGETS_NAMES,
|
|
282
|
-
show_default=True,
|
|
283
|
-
)
|
|
284
|
-
@click.option(
|
|
390
|
+
@grouped_option(
|
|
285
391
|
"-x",
|
|
286
392
|
"--exitfirst",
|
|
287
393
|
"exit_first",
|
|
288
394
|
is_flag=True,
|
|
289
395
|
default=False,
|
|
290
|
-
help="
|
|
396
|
+
help="Terminate the test suite immediately upon the first failure or error encountered",
|
|
291
397
|
show_default=True,
|
|
292
398
|
)
|
|
293
|
-
@
|
|
399
|
+
@grouped_option(
|
|
294
400
|
"--max-failures",
|
|
295
401
|
"max_failures",
|
|
296
402
|
type=click.IntRange(min=1),
|
|
297
|
-
help="
|
|
403
|
+
help="Terminate the test suite after reaching a specified number of failures or errors",
|
|
298
404
|
show_default=True,
|
|
299
405
|
)
|
|
300
|
-
@
|
|
301
|
-
|
|
302
|
-
"
|
|
303
|
-
|
|
406
|
+
@group("Loader options")
|
|
407
|
+
@grouped_option(
|
|
408
|
+
"--app",
|
|
409
|
+
help="Specify the WSGI/ASGI application under test, provided as an importable Python path",
|
|
410
|
+
type=str,
|
|
411
|
+
callback=callbacks.validate_app,
|
|
412
|
+
)
|
|
413
|
+
@grouped_option(
|
|
414
|
+
"--wait-for-schema",
|
|
415
|
+
help="Maximum duration, in seconds, to wait for the API schema to become available. Disabled by default",
|
|
416
|
+
type=click.FloatRange(1.0),
|
|
417
|
+
default=None,
|
|
418
|
+
envvar=WAIT_FOR_SCHEMA_ENV_VAR,
|
|
419
|
+
)
|
|
420
|
+
@grouped_option(
|
|
421
|
+
"--validate-schema",
|
|
422
|
+
help="Validate input API schema. Set to 'true' to enable or 'false' to disable",
|
|
423
|
+
type=bool,
|
|
304
424
|
default=False,
|
|
305
|
-
|
|
425
|
+
show_default=True,
|
|
306
426
|
)
|
|
307
|
-
@
|
|
427
|
+
@group("Network requests options")
|
|
428
|
+
@grouped_option(
|
|
429
|
+
"--base-url",
|
|
430
|
+
"-b",
|
|
431
|
+
help="Base URL of the API, required when schema is provided as a file",
|
|
432
|
+
type=str,
|
|
433
|
+
callback=callbacks.validate_base_url,
|
|
434
|
+
envvar=BASE_URL_ENV_VAR,
|
|
435
|
+
)
|
|
436
|
+
@grouped_option(
|
|
437
|
+
"--request-timeout",
|
|
438
|
+
help="Timeout limit, in milliseconds, for each network request during tests",
|
|
439
|
+
type=click.IntRange(1),
|
|
440
|
+
default=DEFAULT_RESPONSE_TIMEOUT,
|
|
441
|
+
)
|
|
442
|
+
@with_request_proxy
|
|
443
|
+
@with_request_tls_verify
|
|
444
|
+
@with_request_cert
|
|
445
|
+
@with_request_cert_key
|
|
446
|
+
@grouped_option(
|
|
447
|
+
"--rate-limit",
|
|
448
|
+
help="Specify a rate limit for test requests in '<limit>/<duration>' format. "
|
|
449
|
+
"Example - `100/m` for 100 requests per minute",
|
|
450
|
+
type=str,
|
|
451
|
+
callback=callbacks.validate_rate_limit,
|
|
452
|
+
)
|
|
453
|
+
@grouped_option(
|
|
454
|
+
"--header",
|
|
455
|
+
"-H",
|
|
456
|
+
"headers",
|
|
457
|
+
help=r"Add a custom HTTP header to all API requests. Format: 'Header-Name: Value'",
|
|
458
|
+
multiple=True,
|
|
459
|
+
type=str,
|
|
460
|
+
callback=callbacks.validate_headers,
|
|
461
|
+
)
|
|
462
|
+
@grouped_option(
|
|
308
463
|
"--auth",
|
|
309
464
|
"-a",
|
|
310
|
-
help="
|
|
465
|
+
help="Provide the server authentication details in the 'USER:PASSWORD' format",
|
|
311
466
|
type=str,
|
|
312
467
|
callback=callbacks.validate_auth,
|
|
313
468
|
)
|
|
314
|
-
@
|
|
469
|
+
@grouped_option(
|
|
315
470
|
"--auth-type",
|
|
316
471
|
"-A",
|
|
317
472
|
type=click.Choice(["basic", "digest"], case_sensitive=False),
|
|
318
473
|
default="basic",
|
|
319
|
-
help="
|
|
474
|
+
help="Specify the authentication method. For custom authentication methods, see our Authentication documentation: https://schemathesis.readthedocs.io/en/stable/auth.html#custom-auth",
|
|
320
475
|
show_default=True,
|
|
476
|
+
metavar="",
|
|
321
477
|
)
|
|
322
|
-
@
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
type=str,
|
|
328
|
-
callback=callbacks.validate_set_query,
|
|
329
|
-
)
|
|
330
|
-
@click.option(
|
|
331
|
-
"--set-header",
|
|
332
|
-
"set_header",
|
|
333
|
-
help=r"OpenAPI: Override a specific header parameter by specifying 'parameter=value'",
|
|
334
|
-
multiple=True,
|
|
335
|
-
type=str,
|
|
336
|
-
callback=callbacks.validate_set_header,
|
|
337
|
-
)
|
|
338
|
-
@click.option(
|
|
339
|
-
"--set-cookie",
|
|
340
|
-
"set_cookie",
|
|
341
|
-
help=r"OpenAPI: Override a specific cookie parameter by specifying 'parameter=value'",
|
|
342
|
-
multiple=True,
|
|
478
|
+
@group("Filtering options")
|
|
479
|
+
@with_filters
|
|
480
|
+
@grouped_option(
|
|
481
|
+
"--include-by",
|
|
482
|
+
"include_by",
|
|
343
483
|
type=str,
|
|
344
|
-
|
|
484
|
+
help="Include API operations by expression",
|
|
345
485
|
)
|
|
346
|
-
@
|
|
347
|
-
"--
|
|
348
|
-
"
|
|
349
|
-
help=r"OpenAPI: Override a specific path parameter by specifying 'parameter=value'",
|
|
350
|
-
multiple=True,
|
|
486
|
+
@grouped_option(
|
|
487
|
+
"--exclude-by",
|
|
488
|
+
"exclude_by",
|
|
351
489
|
type=str,
|
|
352
|
-
|
|
490
|
+
help="Exclude API operations by expression",
|
|
353
491
|
)
|
|
354
|
-
@
|
|
355
|
-
"--
|
|
356
|
-
"
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
callback=callbacks.validate_headers,
|
|
492
|
+
@grouped_option(
|
|
493
|
+
"--exclude-deprecated",
|
|
494
|
+
help="Exclude deprecated API operations from testing",
|
|
495
|
+
is_flag=True,
|
|
496
|
+
is_eager=True,
|
|
497
|
+
default=False,
|
|
498
|
+
show_default=True,
|
|
362
499
|
)
|
|
363
|
-
@
|
|
500
|
+
@grouped_option(
|
|
364
501
|
"--endpoint",
|
|
365
502
|
"-E",
|
|
366
503
|
"endpoints",
|
|
367
504
|
type=str,
|
|
368
505
|
multiple=True,
|
|
369
|
-
help=r"API operation path pattern (e.g., users/\d+)
|
|
506
|
+
help=r"[DEPRECATED] API operation path pattern (e.g., users/\d+)",
|
|
370
507
|
callback=callbacks.validate_regex,
|
|
371
|
-
|
|
372
|
-
group=ParameterGroup.filtering,
|
|
508
|
+
hidden=True,
|
|
373
509
|
)
|
|
374
|
-
@
|
|
510
|
+
@grouped_option(
|
|
375
511
|
"--method",
|
|
376
512
|
"-M",
|
|
377
513
|
"methods",
|
|
378
514
|
type=str,
|
|
379
515
|
multiple=True,
|
|
380
|
-
help="HTTP method (e.g., GET, POST)
|
|
516
|
+
help="[DEPRECATED] HTTP method (e.g., GET, POST)",
|
|
381
517
|
callback=callbacks.validate_regex,
|
|
382
|
-
|
|
383
|
-
group=ParameterGroup.filtering,
|
|
518
|
+
hidden=True,
|
|
384
519
|
)
|
|
385
|
-
@
|
|
520
|
+
@grouped_option(
|
|
386
521
|
"--tag",
|
|
387
522
|
"-T",
|
|
388
523
|
"tags",
|
|
389
524
|
type=str,
|
|
390
525
|
multiple=True,
|
|
391
|
-
help="Schema tag pattern
|
|
526
|
+
help="[DEPRECATED] Schema tag pattern",
|
|
392
527
|
callback=callbacks.validate_regex,
|
|
393
|
-
|
|
394
|
-
group=ParameterGroup.filtering,
|
|
528
|
+
hidden=True,
|
|
395
529
|
)
|
|
396
|
-
@
|
|
530
|
+
@grouped_option(
|
|
397
531
|
"--operation-id",
|
|
398
532
|
"-O",
|
|
399
533
|
"operation_ids",
|
|
400
534
|
type=str,
|
|
401
535
|
multiple=True,
|
|
402
|
-
help="OpenAPI operationId pattern
|
|
536
|
+
help="[DEPRECATED] OpenAPI operationId pattern",
|
|
403
537
|
callback=callbacks.validate_regex,
|
|
404
|
-
|
|
405
|
-
group=ParameterGroup.filtering,
|
|
406
|
-
)
|
|
407
|
-
@click.option(
|
|
408
|
-
"--workers",
|
|
409
|
-
"-w",
|
|
410
|
-
"workers_num",
|
|
411
|
-
help="Sets the number of concurrent workers for testing. Auto-adjusts if 'auto' is specified.",
|
|
412
|
-
type=CustomHelpMessageChoice(
|
|
413
|
-
["auto"] + list(map(str, range(MIN_WORKERS, MAX_WORKERS + 1))),
|
|
414
|
-
choices_repr=f"[auto|{MIN_WORKERS}-{MAX_WORKERS}]",
|
|
415
|
-
),
|
|
416
|
-
default=str(DEFAULT_WORKERS),
|
|
417
|
-
show_default=True,
|
|
418
|
-
callback=callbacks.convert_workers,
|
|
419
|
-
)
|
|
420
|
-
@click.option(
|
|
421
|
-
"--base-url",
|
|
422
|
-
"-b",
|
|
423
|
-
help="Provides the base URL of the API, required when schema is provided as a file.",
|
|
424
|
-
type=str,
|
|
425
|
-
callback=callbacks.validate_base_url,
|
|
426
|
-
envvar=BASE_URL_ENV_VAR,
|
|
427
|
-
)
|
|
428
|
-
@click.option(
|
|
429
|
-
"--app",
|
|
430
|
-
help="Specifies the WSGI/ASGI application under test, provided as an importable Python path.",
|
|
431
|
-
type=str,
|
|
432
|
-
callback=callbacks.validate_app,
|
|
433
|
-
)
|
|
434
|
-
@click.option(
|
|
435
|
-
"--wait-for-schema",
|
|
436
|
-
help="Maximum duration, in seconds, to wait for the API schema to become available.",
|
|
437
|
-
type=click.FloatRange(1.0),
|
|
438
|
-
default=None,
|
|
439
|
-
envvar=WAIT_FOR_SCHEMA_ENV_VAR,
|
|
440
|
-
)
|
|
441
|
-
@click.option(
|
|
442
|
-
"--request-timeout",
|
|
443
|
-
help="Sets a timeout limit, in milliseconds, for each network request during tests.",
|
|
444
|
-
type=click.IntRange(1),
|
|
445
|
-
default=DEFAULT_RESPONSE_TIMEOUT,
|
|
446
|
-
)
|
|
447
|
-
@with_request_proxy
|
|
448
|
-
@with_request_tls_verify
|
|
449
|
-
@with_request_cert
|
|
450
|
-
@with_request_cert_key
|
|
451
|
-
@click.option(
|
|
452
|
-
"--validate-schema",
|
|
453
|
-
help="Toggles validation of incoming payloads against the defined API schema. "
|
|
454
|
-
"Set to 'True' to enable or 'False' to disable. "
|
|
455
|
-
"Default is 'False'.",
|
|
456
|
-
type=bool,
|
|
457
|
-
default=False,
|
|
458
|
-
show_default=True,
|
|
459
|
-
cls=GroupedOption,
|
|
460
|
-
group=ParameterGroup.validation,
|
|
538
|
+
hidden=True,
|
|
461
539
|
)
|
|
462
|
-
@
|
|
540
|
+
@grouped_option(
|
|
463
541
|
"--skip-deprecated-operations",
|
|
464
|
-
help="Exclude deprecated API operations from testing
|
|
542
|
+
help="[DEPRECATED] Exclude deprecated API operations from testing",
|
|
465
543
|
is_flag=True,
|
|
466
544
|
is_eager=True,
|
|
467
545
|
default=False,
|
|
468
546
|
show_default=True,
|
|
469
|
-
|
|
470
|
-
group=ParameterGroup.filtering,
|
|
547
|
+
hidden=True,
|
|
471
548
|
)
|
|
472
|
-
@
|
|
549
|
+
@group("Output options")
|
|
550
|
+
@grouped_option(
|
|
473
551
|
"--junit-xml",
|
|
474
|
-
help="
|
|
552
|
+
help="Output a JUnit-XML style report at the specified file path",
|
|
475
553
|
type=click.File("w", encoding="utf-8"),
|
|
476
554
|
)
|
|
477
|
-
@
|
|
478
|
-
"--
|
|
479
|
-
"
|
|
480
|
-
help="""Specifies how the generated report should be handled.
|
|
481
|
-
If used without an argument, the report data will automatically be uploaded to Schemathesis.io.
|
|
482
|
-
If a file name is provided, the report will be stored in that file.
|
|
483
|
-
The report data, consisting of a tar gz file with multiple JSON files, is subject to change.""",
|
|
484
|
-
is_flag=False,
|
|
485
|
-
flag_value="",
|
|
486
|
-
envvar=service.REPORT_ENV_VAR,
|
|
487
|
-
callback=callbacks.convert_report, # type: ignore
|
|
488
|
-
)
|
|
489
|
-
@click.option(
|
|
490
|
-
"--debug-output-file",
|
|
491
|
-
help="Saves debugging information in a JSONL format at the specified file path.",
|
|
555
|
+
@grouped_option(
|
|
556
|
+
"--cassette-path",
|
|
557
|
+
help="Save the test outcomes in a VCR-compatible format",
|
|
492
558
|
type=click.File("w", encoding="utf-8"),
|
|
493
|
-
)
|
|
494
|
-
@click.option(
|
|
495
|
-
"--show-errors-tracebacks",
|
|
496
|
-
help="Displays complete traceback information for internal errors.",
|
|
497
|
-
is_flag=True,
|
|
498
559
|
is_eager=True,
|
|
499
|
-
default=False,
|
|
500
|
-
hidden=True,
|
|
501
|
-
show_default=True,
|
|
502
560
|
)
|
|
503
|
-
@
|
|
504
|
-
"--
|
|
505
|
-
help="
|
|
561
|
+
@grouped_option(
|
|
562
|
+
"--cassette-format",
|
|
563
|
+
help="Format of the saved cassettes",
|
|
564
|
+
type=click.Choice([item.name.lower() for item in cassettes.CassetteFormat]),
|
|
565
|
+
default=cassettes.CassetteFormat.VCR.name.lower(),
|
|
566
|
+
callback=callbacks.convert_cassette_format,
|
|
567
|
+
metavar="",
|
|
568
|
+
)
|
|
569
|
+
@grouped_option(
|
|
570
|
+
"--cassette-preserve-exact-body-bytes",
|
|
571
|
+
help="Retain exact byte sequence of payloads in cassettes, encoded as base64",
|
|
506
572
|
is_flag=True,
|
|
507
|
-
|
|
508
|
-
default=False,
|
|
509
|
-
show_default=True,
|
|
573
|
+
callback=callbacks.validate_preserve_exact_body_bytes,
|
|
510
574
|
)
|
|
511
|
-
@
|
|
575
|
+
@grouped_option(
|
|
512
576
|
"--code-sample-style",
|
|
513
|
-
help="
|
|
577
|
+
help="Code sample style for reproducing failures",
|
|
514
578
|
type=click.Choice([item.name for item in CodeSampleStyle]),
|
|
515
579
|
default=CodeSampleStyle.default().name,
|
|
516
580
|
callback=callbacks.convert_code_sample_style,
|
|
581
|
+
metavar="",
|
|
517
582
|
)
|
|
518
|
-
@
|
|
519
|
-
"--
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
583
|
+
@grouped_option(
|
|
584
|
+
"--sanitize-output",
|
|
585
|
+
type=bool,
|
|
586
|
+
default=True,
|
|
587
|
+
show_default=True,
|
|
588
|
+
help="Enable or disable automatic output sanitization to obscure sensitive data",
|
|
523
589
|
)
|
|
524
|
-
@
|
|
525
|
-
"--
|
|
526
|
-
help="
|
|
590
|
+
@grouped_option(
|
|
591
|
+
"--output-truncate",
|
|
592
|
+
help="Truncate schemas and responses in error messages",
|
|
593
|
+
type=str,
|
|
594
|
+
default="true",
|
|
595
|
+
show_default=True,
|
|
596
|
+
callback=callbacks.convert_boolean_string,
|
|
597
|
+
)
|
|
598
|
+
@grouped_option(
|
|
599
|
+
"--show-trace",
|
|
600
|
+
help="Display complete traceback information for internal errors",
|
|
527
601
|
is_flag=True,
|
|
528
|
-
|
|
602
|
+
is_eager=True,
|
|
603
|
+
default=False,
|
|
604
|
+
show_default=True,
|
|
529
605
|
)
|
|
530
|
-
@
|
|
606
|
+
@grouped_option(
|
|
607
|
+
"--debug-output-file",
|
|
608
|
+
help="Save debugging information in a JSONL format at the specified file path",
|
|
609
|
+
type=click.File("w", encoding="utf-8"),
|
|
610
|
+
)
|
|
611
|
+
@grouped_option(
|
|
531
612
|
"--store-network-log",
|
|
532
|
-
help="
|
|
613
|
+
help="[DEPRECATED] Save the test outcomes in a VCR-compatible format",
|
|
533
614
|
type=click.File("w", encoding="utf-8"),
|
|
534
615
|
hidden=True,
|
|
535
616
|
)
|
|
536
|
-
@
|
|
537
|
-
"--
|
|
538
|
-
help="
|
|
539
|
-
|
|
540
|
-
|
|
617
|
+
@grouped_option(
|
|
618
|
+
"--show-errors-tracebacks",
|
|
619
|
+
help="[DEPRECATED] Display complete traceback information for internal errors",
|
|
620
|
+
is_flag=True,
|
|
621
|
+
is_eager=True,
|
|
622
|
+
default=False,
|
|
623
|
+
hidden=True,
|
|
624
|
+
show_default=True,
|
|
541
625
|
)
|
|
542
|
-
@
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
"
|
|
546
|
-
|
|
547
|
-
|
|
626
|
+
@group("Data generation options")
|
|
627
|
+
@grouped_option(
|
|
628
|
+
"--data-generation-method",
|
|
629
|
+
"-D",
|
|
630
|
+
"data_generation_methods",
|
|
631
|
+
help="Specify the approach Schemathesis uses to generate test data. "
|
|
632
|
+
"Use 'positive' for valid data, 'negative' for invalid data, or 'all' for both",
|
|
633
|
+
type=DATA_GENERATION_METHOD_TYPE,
|
|
634
|
+
default=DataGenerationMethod.default().name,
|
|
635
|
+
callback=callbacks.convert_data_generation_method,
|
|
636
|
+
show_default=True,
|
|
637
|
+
metavar="",
|
|
548
638
|
)
|
|
549
|
-
@
|
|
639
|
+
@grouped_option(
|
|
550
640
|
"--stateful",
|
|
551
|
-
help="
|
|
641
|
+
help="Enable or disable stateful testing",
|
|
552
642
|
type=click.Choice([item.name for item in Stateful]),
|
|
553
643
|
default=Stateful.links.name,
|
|
554
644
|
callback=callbacks.convert_stateful,
|
|
645
|
+
metavar="",
|
|
555
646
|
)
|
|
556
|
-
@
|
|
647
|
+
@grouped_option(
|
|
557
648
|
"--stateful-recursion-limit",
|
|
558
|
-
help="
|
|
649
|
+
help="Recursion depth limit for stateful testing",
|
|
559
650
|
default=DEFAULT_STATEFUL_RECURSION_LIMIT,
|
|
560
651
|
show_default=True,
|
|
561
652
|
type=click.IntRange(1, 100),
|
|
562
653
|
hidden=True,
|
|
563
654
|
)
|
|
564
|
-
@
|
|
565
|
-
"--
|
|
566
|
-
help="
|
|
567
|
-
type=
|
|
655
|
+
@grouped_option(
|
|
656
|
+
"--generation-allow-x00",
|
|
657
|
+
help="Whether to allow the generation of `\x00` bytes within strings",
|
|
658
|
+
type=str,
|
|
659
|
+
default="true",
|
|
660
|
+
show_default=True,
|
|
661
|
+
callback=callbacks.convert_boolean_string,
|
|
568
662
|
)
|
|
569
|
-
@
|
|
570
|
-
"--
|
|
571
|
-
|
|
572
|
-
|
|
663
|
+
@grouped_option(
|
|
664
|
+
"--generation-codec",
|
|
665
|
+
help="The codec used for generating strings",
|
|
666
|
+
type=str,
|
|
667
|
+
default="utf-8",
|
|
668
|
+
callback=callbacks.validate_generation_codec,
|
|
669
|
+
)
|
|
670
|
+
@grouped_option(
|
|
671
|
+
"--generation-with-security-parameters",
|
|
672
|
+
help="Whether to generate security parameters",
|
|
673
|
+
type=str,
|
|
674
|
+
default="true",
|
|
573
675
|
show_default=True,
|
|
574
|
-
|
|
676
|
+
callback=callbacks.convert_boolean_string,
|
|
575
677
|
)
|
|
576
|
-
@
|
|
678
|
+
@grouped_option(
|
|
679
|
+
"--generation-graphql-allow-null",
|
|
680
|
+
help="Whether to use `null` values for optional arguments in GraphQL queries",
|
|
681
|
+
type=str,
|
|
682
|
+
default="true",
|
|
683
|
+
show_default=True,
|
|
684
|
+
callback=callbacks.convert_boolean_string,
|
|
685
|
+
)
|
|
686
|
+
@grouped_option(
|
|
577
687
|
"--contrib-unique-data",
|
|
578
688
|
"contrib_unique_data",
|
|
579
|
-
help="
|
|
689
|
+
help="Force the generation of unique test cases",
|
|
580
690
|
is_flag=True,
|
|
581
691
|
default=False,
|
|
582
692
|
show_default=True,
|
|
583
693
|
)
|
|
584
|
-
@
|
|
694
|
+
@grouped_option(
|
|
585
695
|
"--contrib-openapi-formats-uuid",
|
|
586
696
|
"contrib_openapi_formats_uuid",
|
|
587
|
-
help="
|
|
697
|
+
help="Enable support for the 'uuid' string format in OpenAPI",
|
|
588
698
|
is_flag=True,
|
|
589
699
|
default=False,
|
|
590
700
|
show_default=True,
|
|
591
701
|
)
|
|
592
|
-
@
|
|
702
|
+
@grouped_option(
|
|
593
703
|
"--contrib-openapi-fill-missing-examples",
|
|
594
704
|
"contrib_openapi_fill_missing_examples",
|
|
595
|
-
help="
|
|
705
|
+
help="Enable generation of random examples for API operations that do not have explicit examples",
|
|
596
706
|
is_flag=True,
|
|
597
707
|
default=False,
|
|
598
708
|
show_default=True,
|
|
599
709
|
)
|
|
600
|
-
@
|
|
710
|
+
@grouped_option(
|
|
711
|
+
"--target",
|
|
712
|
+
"-t",
|
|
713
|
+
"targets",
|
|
714
|
+
multiple=True,
|
|
715
|
+
help="Guide input generation to values more likely to expose bugs via targeted property-based testing",
|
|
716
|
+
type=TARGETS_TYPE,
|
|
717
|
+
default=DEFAULT_TARGETS_NAMES,
|
|
718
|
+
show_default=True,
|
|
719
|
+
metavar="",
|
|
720
|
+
)
|
|
721
|
+
@group("Open API options")
|
|
722
|
+
@grouped_option(
|
|
723
|
+
"--force-schema-version",
|
|
724
|
+
help="Force the schema to be interpreted as a particular OpenAPI version",
|
|
725
|
+
type=click.Choice(["20", "30"]),
|
|
726
|
+
metavar="",
|
|
727
|
+
)
|
|
728
|
+
@grouped_option(
|
|
729
|
+
"--set-query",
|
|
730
|
+
"set_query",
|
|
731
|
+
help=r"OpenAPI: Override a specific query parameter by specifying 'parameter=value'",
|
|
732
|
+
multiple=True,
|
|
733
|
+
type=str,
|
|
734
|
+
callback=callbacks.validate_set_query,
|
|
735
|
+
)
|
|
736
|
+
@grouped_option(
|
|
737
|
+
"--set-header",
|
|
738
|
+
"set_header",
|
|
739
|
+
help=r"OpenAPI: Override a specific header parameter by specifying 'parameter=value'",
|
|
740
|
+
multiple=True,
|
|
741
|
+
type=str,
|
|
742
|
+
callback=callbacks.validate_set_header,
|
|
743
|
+
)
|
|
744
|
+
@grouped_option(
|
|
745
|
+
"--set-cookie",
|
|
746
|
+
"set_cookie",
|
|
747
|
+
help=r"OpenAPI: Override a specific cookie parameter by specifying 'parameter=value'",
|
|
748
|
+
multiple=True,
|
|
749
|
+
type=str,
|
|
750
|
+
callback=callbacks.validate_set_cookie,
|
|
751
|
+
)
|
|
752
|
+
@grouped_option(
|
|
753
|
+
"--set-path",
|
|
754
|
+
"set_path",
|
|
755
|
+
help=r"OpenAPI: Override a specific path parameter by specifying 'parameter=value'",
|
|
756
|
+
multiple=True,
|
|
757
|
+
type=str,
|
|
758
|
+
callback=callbacks.validate_set_path,
|
|
759
|
+
)
|
|
760
|
+
@group("Hypothesis engine options")
|
|
761
|
+
@grouped_option(
|
|
601
762
|
"--hypothesis-database",
|
|
602
|
-
help="
|
|
763
|
+
help="Storage for examples discovered by Hypothesis. "
|
|
603
764
|
f"Use 'none' to disable, '{HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER}' for temporary storage, "
|
|
604
|
-
f"or specify a file path for persistent storage
|
|
765
|
+
f"or specify a file path for persistent storage",
|
|
605
766
|
type=str,
|
|
606
|
-
cls=GroupedOption,
|
|
607
|
-
group=ParameterGroup.hypothesis,
|
|
608
767
|
callback=callbacks.validate_hypothesis_database,
|
|
609
768
|
)
|
|
610
|
-
@
|
|
769
|
+
@grouped_option(
|
|
611
770
|
"--hypothesis-deadline",
|
|
612
|
-
help="
|
|
613
|
-
"Exceeding this limit will cause the test to fail
|
|
614
|
-
|
|
615
|
-
type=OptionalInt(1, 999999999 * 24 * 3600 * 1000),
|
|
616
|
-
cls=GroupedOption,
|
|
617
|
-
group=ParameterGroup.hypothesis,
|
|
771
|
+
help="Time limit for each test case generated by Hypothesis, in milliseconds. "
|
|
772
|
+
"Exceeding this limit will cause the test to fail",
|
|
773
|
+
type=OptionalInt(1, 5 * 60 * 1000),
|
|
618
774
|
)
|
|
619
|
-
@
|
|
775
|
+
@grouped_option(
|
|
620
776
|
"--hypothesis-derandomize",
|
|
621
|
-
help="Enables deterministic mode in Hypothesis, which eliminates random variation between
|
|
777
|
+
help="Enables deterministic mode in Hypothesis, which eliminates random variation between tests",
|
|
622
778
|
is_flag=True,
|
|
623
779
|
is_eager=True,
|
|
624
780
|
default=None,
|
|
625
781
|
show_default=True,
|
|
626
|
-
cls=GroupedOption,
|
|
627
|
-
group=ParameterGroup.hypothesis,
|
|
628
782
|
)
|
|
629
|
-
@
|
|
783
|
+
@grouped_option(
|
|
630
784
|
"--hypothesis-max-examples",
|
|
631
|
-
help="
|
|
785
|
+
help="The cap on the number of examples generated by Hypothesis for each API operation",
|
|
632
786
|
type=click.IntRange(1),
|
|
633
|
-
cls=GroupedOption,
|
|
634
|
-
group=ParameterGroup.hypothesis,
|
|
635
787
|
)
|
|
636
|
-
@
|
|
788
|
+
@grouped_option(
|
|
637
789
|
"--hypothesis-phases",
|
|
638
|
-
help="
|
|
790
|
+
help="Testing phases to execute",
|
|
639
791
|
type=CsvEnumChoice(Phase),
|
|
640
|
-
|
|
641
|
-
group=ParameterGroup.hypothesis,
|
|
792
|
+
metavar="",
|
|
642
793
|
)
|
|
643
|
-
@
|
|
794
|
+
@grouped_option(
|
|
644
795
|
"--hypothesis-no-phases",
|
|
645
|
-
help="
|
|
796
|
+
help="Testing phases to exclude from execution",
|
|
646
797
|
type=CsvEnumChoice(Phase),
|
|
647
|
-
|
|
648
|
-
group=ParameterGroup.hypothesis,
|
|
798
|
+
metavar="",
|
|
649
799
|
)
|
|
650
|
-
@
|
|
800
|
+
@grouped_option(
|
|
651
801
|
"--hypothesis-report-multiple-bugs",
|
|
652
|
-
help="
|
|
802
|
+
help="Report only the most easily reproducible error when multiple issues are found",
|
|
653
803
|
type=bool,
|
|
654
|
-
cls=GroupedOption,
|
|
655
|
-
group=ParameterGroup.hypothesis,
|
|
656
804
|
)
|
|
657
|
-
@
|
|
805
|
+
@grouped_option(
|
|
658
806
|
"--hypothesis-seed",
|
|
659
|
-
help="
|
|
807
|
+
help="Seed value for Hypothesis, ensuring reproducibility across test runs",
|
|
660
808
|
type=int,
|
|
661
|
-
cls=GroupedOption,
|
|
662
|
-
group=ParameterGroup.hypothesis,
|
|
663
809
|
)
|
|
664
|
-
@
|
|
810
|
+
@grouped_option(
|
|
665
811
|
"--hypothesis-suppress-health-check",
|
|
666
|
-
help="
|
|
667
|
-
"Provide a comma-separated list",
|
|
812
|
+
help="A comma-separated list of Hypothesis health checks to disable",
|
|
668
813
|
type=CsvEnumChoice(HealthCheck),
|
|
669
|
-
|
|
670
|
-
group=ParameterGroup.hypothesis,
|
|
814
|
+
metavar="",
|
|
671
815
|
)
|
|
672
|
-
@
|
|
816
|
+
@grouped_option(
|
|
673
817
|
"--hypothesis-verbosity",
|
|
674
|
-
help="
|
|
818
|
+
help="Verbosity level of Hypothesis output",
|
|
675
819
|
type=click.Choice([item.name for item in Verbosity]),
|
|
676
820
|
callback=callbacks.convert_verbosity,
|
|
677
|
-
|
|
678
|
-
group=ParameterGroup.hypothesis,
|
|
821
|
+
metavar="",
|
|
679
822
|
)
|
|
680
|
-
@
|
|
681
|
-
@
|
|
682
|
-
|
|
683
|
-
"
|
|
684
|
-
help="
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
type=str,
|
|
693
|
-
default="true",
|
|
694
|
-
show_default=True,
|
|
695
|
-
callback=callbacks.convert_boolean_string,
|
|
696
|
-
)
|
|
697
|
-
@click.option(
|
|
698
|
-
"--generation-codec",
|
|
699
|
-
help="Specifies the codec used for generating strings.",
|
|
700
|
-
type=str,
|
|
701
|
-
default="utf-8",
|
|
702
|
-
callback=callbacks.validate_generation_codec,
|
|
823
|
+
@group("Schemathesis.io options")
|
|
824
|
+
@grouped_option(
|
|
825
|
+
"--report",
|
|
826
|
+
"report_value",
|
|
827
|
+
help="""Specify how the generated report should be handled.
|
|
828
|
+
If used without an argument, the report data will automatically be uploaded to Schemathesis.io.
|
|
829
|
+
If a file name is provided, the report will be stored in that file.
|
|
830
|
+
The report data, consisting of a tar gz file with multiple JSON files, is subject to change""",
|
|
831
|
+
is_flag=False,
|
|
832
|
+
flag_value="",
|
|
833
|
+
envvar=service.REPORT_ENV_VAR,
|
|
834
|
+
callback=callbacks.convert_report, # type: ignore
|
|
703
835
|
)
|
|
704
|
-
@
|
|
836
|
+
@grouped_option(
|
|
705
837
|
"--schemathesis-io-token",
|
|
706
|
-
help="Schemathesis.io authentication token
|
|
838
|
+
help="Schemathesis.io authentication token",
|
|
707
839
|
type=str,
|
|
708
840
|
envvar=service.TOKEN_ENV_VAR,
|
|
709
841
|
)
|
|
710
|
-
@
|
|
842
|
+
@grouped_option(
|
|
711
843
|
"--schemathesis-io-url",
|
|
712
|
-
help="Schemathesis.io base URL
|
|
844
|
+
help="Schemathesis.io base URL",
|
|
713
845
|
default=service.DEFAULT_URL,
|
|
714
846
|
type=str,
|
|
715
847
|
envvar=service.URL_ENV_VAR,
|
|
716
848
|
)
|
|
717
|
-
@
|
|
849
|
+
@grouped_option(
|
|
718
850
|
"--schemathesis-io-telemetry",
|
|
719
|
-
help="
|
|
851
|
+
help="Whether to send anonymized usage data to Schemathesis.io along with your report",
|
|
720
852
|
type=str,
|
|
721
853
|
default="true",
|
|
722
854
|
show_default=True,
|
|
@@ -724,7 +856,10 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
|
|
|
724
856
|
envvar=service.TELEMETRY_ENV_VAR,
|
|
725
857
|
)
|
|
726
858
|
@with_hosts_file
|
|
727
|
-
@
|
|
859
|
+
@group("Global options")
|
|
860
|
+
@grouped_option("--verbosity", "-v", help="Increase verbosity of the output", count=True)
|
|
861
|
+
@grouped_option("--no-color", help="Disable ANSI color escape codes", type=bool, is_flag=True)
|
|
862
|
+
@grouped_option("--force-color", help="Explicitly tells to enable ANSI color escape codes", type=bool, is_flag=True)
|
|
728
863
|
@click.pass_context
|
|
729
864
|
def run(
|
|
730
865
|
ctx: click.Context,
|
|
@@ -737,7 +872,11 @@ def run(
|
|
|
737
872
|
set_header: dict[str, str],
|
|
738
873
|
set_cookie: dict[str, str],
|
|
739
874
|
set_path: dict[str, str],
|
|
740
|
-
|
|
875
|
+
experiments: list,
|
|
876
|
+
no_failfast: bool,
|
|
877
|
+
missing_required_header_allowed_statuses: list[str],
|
|
878
|
+
positive_data_acceptance_allowed_statuses: list[str],
|
|
879
|
+
negative_data_rejection_allowed_statuses: list[str],
|
|
741
880
|
checks: Iterable[str] = DEFAULT_CHECKS_NAMES,
|
|
742
881
|
exclude_checks: Iterable[str] = (),
|
|
743
882
|
data_generation_methods: tuple[DataGenerationMethod, ...] = DEFAULT_DATA_GENERATION_METHODS,
|
|
@@ -746,10 +885,33 @@ def run(
|
|
|
746
885
|
exit_first: bool = False,
|
|
747
886
|
max_failures: int | None = None,
|
|
748
887
|
dry_run: bool = False,
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
888
|
+
include_path: Sequence[str] = (),
|
|
889
|
+
include_path_regex: str | None = None,
|
|
890
|
+
include_method: Sequence[str] = (),
|
|
891
|
+
include_method_regex: str | None = None,
|
|
892
|
+
include_name: Sequence[str] = (),
|
|
893
|
+
include_name_regex: str | None = None,
|
|
894
|
+
include_tag: Sequence[str] = (),
|
|
895
|
+
include_tag_regex: str | None = None,
|
|
896
|
+
include_operation_id: Sequence[str] = (),
|
|
897
|
+
include_operation_id_regex: str | None = None,
|
|
898
|
+
exclude_path: Sequence[str] = (),
|
|
899
|
+
exclude_path_regex: str | None = None,
|
|
900
|
+
exclude_method: Sequence[str] = (),
|
|
901
|
+
exclude_method_regex: str | None = None,
|
|
902
|
+
exclude_name: Sequence[str] = (),
|
|
903
|
+
exclude_name_regex: str | None = None,
|
|
904
|
+
exclude_tag: Sequence[str] = (),
|
|
905
|
+
exclude_tag_regex: str | None = None,
|
|
906
|
+
exclude_operation_id: Sequence[str] = (),
|
|
907
|
+
exclude_operation_id_regex: str | None = None,
|
|
908
|
+
include_by: str | None = None,
|
|
909
|
+
exclude_by: str | None = None,
|
|
910
|
+
exclude_deprecated: bool = False,
|
|
911
|
+
endpoints: tuple[str, ...] = (),
|
|
912
|
+
methods: tuple[str, ...] = (),
|
|
913
|
+
tags: tuple[str, ...] = (),
|
|
914
|
+
operation_ids: tuple[str, ...] = (),
|
|
753
915
|
workers_num: int = DEFAULT_WORKERS,
|
|
754
916
|
base_url: str | None = None,
|
|
755
917
|
app: str | None = None,
|
|
@@ -766,6 +928,7 @@ def run(
|
|
|
766
928
|
show_trace: bool = False,
|
|
767
929
|
code_sample_style: CodeSampleStyle = CodeSampleStyle.default(),
|
|
768
930
|
cassette_path: click.utils.LazyFile | None = None,
|
|
931
|
+
cassette_format: cassettes.CassetteFormat = cassettes.CassetteFormat.VCR,
|
|
769
932
|
cassette_preserve_exact_body_bytes: bool = False,
|
|
770
933
|
store_network_log: click.utils.LazyFile | None = None,
|
|
771
934
|
wait_for_schema: float | None = None,
|
|
@@ -775,6 +938,7 @@ def run(
|
|
|
775
938
|
stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT,
|
|
776
939
|
force_schema_version: str | None = None,
|
|
777
940
|
sanitize_output: bool = True,
|
|
941
|
+
output_truncate: bool = True,
|
|
778
942
|
contrib_unique_data: bool = False,
|
|
779
943
|
contrib_openapi_formats_uuid: bool = False,
|
|
780
944
|
contrib_openapi_fill_missing_examples: bool = False,
|
|
@@ -792,18 +956,21 @@ def run(
|
|
|
792
956
|
no_color: bool = False,
|
|
793
957
|
report_value: str | None = None,
|
|
794
958
|
generation_allow_x00: bool = True,
|
|
959
|
+
generation_graphql_allow_null: bool = True,
|
|
960
|
+
generation_with_security_parameters: bool = True,
|
|
795
961
|
generation_codec: str = "utf-8",
|
|
796
962
|
schemathesis_io_token: str | None = None,
|
|
797
963
|
schemathesis_io_url: str = service.DEFAULT_URL,
|
|
798
964
|
schemathesis_io_telemetry: bool = True,
|
|
799
965
|
hosts_file: PathLike = service.DEFAULT_HOSTS_PATH,
|
|
800
966
|
force_color: bool = False,
|
|
967
|
+
**__kwargs,
|
|
801
968
|
) -> None:
|
|
802
969
|
"""Run tests against an API using a specified SCHEMA.
|
|
803
970
|
|
|
804
|
-
[Required] SCHEMA: Path to an OpenAPI (`.json`, `.yml`) or GraphQL SDL file, or a URL pointing to such specifications
|
|
971
|
+
[Required] SCHEMA: Path to an OpenAPI (`.json`, `.yml`) or GraphQL SDL file, or a URL pointing to such specifications
|
|
805
972
|
|
|
806
|
-
[Optional] API_NAME: Identifier for uploading test data to Schemathesis.io
|
|
973
|
+
[Optional] API_NAME: Identifier for uploading test data to Schemathesis.io
|
|
807
974
|
"""
|
|
808
975
|
_hypothesis_phases: list[hypothesis.Phase] | None = None
|
|
809
976
|
if hypothesis_phases is not None:
|
|
@@ -815,23 +982,25 @@ def run(
|
|
|
815
982
|
_hypothesis_suppress_health_check: list[hypothesis.HealthCheck] | None = None
|
|
816
983
|
if hypothesis_suppress_health_check is not None:
|
|
817
984
|
_hypothesis_suppress_health_check = [
|
|
818
|
-
|
|
985
|
+
entry for health_check in hypothesis_suppress_health_check for entry in health_check.as_hypothesis()
|
|
819
986
|
]
|
|
820
987
|
|
|
821
|
-
if contrib_unique_data:
|
|
822
|
-
click.secho(DEPRECATED_CONTRIB_UNIQUE_DATA_OPTION_WARNING, fg="yellow")
|
|
823
|
-
|
|
824
988
|
if show_errors_tracebacks:
|
|
825
989
|
click.secho(DEPRECATED_SHOW_ERROR_TRACEBACKS_OPTION_WARNING, fg="yellow")
|
|
826
990
|
show_trace = show_errors_tracebacks
|
|
827
991
|
|
|
828
992
|
# Enable selected experiments
|
|
829
|
-
for experiment in
|
|
993
|
+
for experiment in experiments:
|
|
830
994
|
experiment.enable()
|
|
831
995
|
|
|
832
996
|
override = CaseOverride(query=set_query, headers=set_header, cookies=set_cookie, path_parameters=set_path)
|
|
833
997
|
|
|
834
|
-
generation_config = generation.GenerationConfig(
|
|
998
|
+
generation_config = generation.GenerationConfig(
|
|
999
|
+
allow_x00=generation_allow_x00,
|
|
1000
|
+
graphql_allow_null=generation_graphql_allow_null,
|
|
1001
|
+
codec=generation_codec,
|
|
1002
|
+
with_security_parameters=generation_with_security_parameters,
|
|
1003
|
+
)
|
|
835
1004
|
|
|
836
1005
|
report: ReportToService | click.utils.LazyFile | None
|
|
837
1006
|
if report_value is None:
|
|
@@ -855,6 +1024,109 @@ def run(
|
|
|
855
1024
|
click.secho(DEPRECATED_CASSETTE_PATH_OPTION_WARNING, fg="yellow")
|
|
856
1025
|
cassette_path = store_network_log
|
|
857
1026
|
|
|
1027
|
+
output_config = OutputConfig(truncate=output_truncate)
|
|
1028
|
+
|
|
1029
|
+
deprecated_filters = {
|
|
1030
|
+
"--method": "--include-method",
|
|
1031
|
+
"--endpoint": "--include-path",
|
|
1032
|
+
"--tag": "--include-tag",
|
|
1033
|
+
"--operation-id": "--include-operation-id",
|
|
1034
|
+
}
|
|
1035
|
+
for values, arg_name in (
|
|
1036
|
+
(include_path, "--include-path"),
|
|
1037
|
+
(include_method, "--include-method"),
|
|
1038
|
+
(include_name, "--include-name"),
|
|
1039
|
+
(include_tag, "--include-tag"),
|
|
1040
|
+
(include_operation_id, "--include-operation-id"),
|
|
1041
|
+
(exclude_path, "--exclude-path"),
|
|
1042
|
+
(exclude_method, "--exclude-method"),
|
|
1043
|
+
(exclude_name, "--exclude-name"),
|
|
1044
|
+
(exclude_tag, "--exclude-tag"),
|
|
1045
|
+
(exclude_operation_id, "--exclude-operation-id"),
|
|
1046
|
+
(methods, "--method"),
|
|
1047
|
+
(endpoints, "--endpoint"),
|
|
1048
|
+
(tags, "--tag"),
|
|
1049
|
+
(operation_ids, "--operation-id"),
|
|
1050
|
+
):
|
|
1051
|
+
if values and arg_name in deprecated_filters:
|
|
1052
|
+
replacement = deprecated_filters[arg_name]
|
|
1053
|
+
click.secho(
|
|
1054
|
+
f"Warning: Option `{arg_name}` is deprecated and will be removed in Schemathesis 4.0. "
|
|
1055
|
+
f"Use `{replacement}` instead",
|
|
1056
|
+
fg="yellow",
|
|
1057
|
+
)
|
|
1058
|
+
_ensure_unique_filter(values, arg_name)
|
|
1059
|
+
include_by_function = _filter_by_expression_to_func(include_by, "--include-by")
|
|
1060
|
+
exclude_by_function = _filter_by_expression_to_func(exclude_by, "--exclude-by")
|
|
1061
|
+
|
|
1062
|
+
filter_set = FilterSet()
|
|
1063
|
+
if include_by_function:
|
|
1064
|
+
filter_set.include(include_by_function)
|
|
1065
|
+
for name_ in include_name:
|
|
1066
|
+
filter_set.include(name=name_)
|
|
1067
|
+
for method in include_method:
|
|
1068
|
+
filter_set.include(method=method)
|
|
1069
|
+
if methods:
|
|
1070
|
+
for method in methods:
|
|
1071
|
+
filter_set.include(method_regex=method)
|
|
1072
|
+
for path in include_path:
|
|
1073
|
+
filter_set.include(path=path)
|
|
1074
|
+
if endpoints:
|
|
1075
|
+
for endpoint in endpoints:
|
|
1076
|
+
filter_set.include(path_regex=endpoint)
|
|
1077
|
+
for tag in include_tag:
|
|
1078
|
+
filter_set.include(tag=tag)
|
|
1079
|
+
if tags:
|
|
1080
|
+
for tag in tags:
|
|
1081
|
+
filter_set.include(tag_regex=tag)
|
|
1082
|
+
for operation_id in include_operation_id:
|
|
1083
|
+
filter_set.include(operation_id=operation_id)
|
|
1084
|
+
if operation_ids:
|
|
1085
|
+
for operation_id in operation_ids:
|
|
1086
|
+
filter_set.include(operation_id_regex=operation_id)
|
|
1087
|
+
if (
|
|
1088
|
+
include_name_regex
|
|
1089
|
+
or include_method_regex
|
|
1090
|
+
or include_path_regex
|
|
1091
|
+
or include_tag_regex
|
|
1092
|
+
or include_operation_id_regex
|
|
1093
|
+
):
|
|
1094
|
+
filter_set.include(
|
|
1095
|
+
name_regex=include_name_regex,
|
|
1096
|
+
method_regex=include_method_regex,
|
|
1097
|
+
path_regex=include_path_regex,
|
|
1098
|
+
tag_regex=include_tag_regex,
|
|
1099
|
+
operation_id_regex=include_operation_id_regex,
|
|
1100
|
+
)
|
|
1101
|
+
if exclude_by_function:
|
|
1102
|
+
filter_set.exclude(exclude_by_function)
|
|
1103
|
+
for name_ in exclude_name:
|
|
1104
|
+
filter_set.exclude(name=name_)
|
|
1105
|
+
for method in exclude_method:
|
|
1106
|
+
filter_set.exclude(method=method)
|
|
1107
|
+
for path in exclude_path:
|
|
1108
|
+
filter_set.exclude(path=path)
|
|
1109
|
+
for tag in exclude_tag:
|
|
1110
|
+
filter_set.exclude(tag=tag)
|
|
1111
|
+
for operation_id in exclude_operation_id:
|
|
1112
|
+
filter_set.exclude(operation_id=operation_id)
|
|
1113
|
+
if (
|
|
1114
|
+
exclude_name_regex
|
|
1115
|
+
or exclude_method_regex
|
|
1116
|
+
or exclude_path_regex
|
|
1117
|
+
or exclude_tag_regex
|
|
1118
|
+
or exclude_operation_id_regex
|
|
1119
|
+
):
|
|
1120
|
+
filter_set.exclude(
|
|
1121
|
+
name_regex=exclude_name_regex,
|
|
1122
|
+
method_regex=exclude_method_regex,
|
|
1123
|
+
path_regex=exclude_path_regex,
|
|
1124
|
+
tag_regex=exclude_tag_regex,
|
|
1125
|
+
operation_id_regex=exclude_operation_id_regex,
|
|
1126
|
+
)
|
|
1127
|
+
if exclude_deprecated or skip_deprecated_operations:
|
|
1128
|
+
filter_set.exclude(is_deprecated)
|
|
1129
|
+
|
|
858
1130
|
schemathesis_io_hostname = urlparse(schemathesis_io_url).netloc
|
|
859
1131
|
token = schemathesis_io_token or service.hosts.get_token(hostname=schemathesis_io_hostname, hosts_file=hosts_file)
|
|
860
1132
|
schema_kind = callbacks.parse_schema_kind(schema, app)
|
|
@@ -901,6 +1173,10 @@ def run(
|
|
|
901
1173
|
from ..service.client import ServiceClient
|
|
902
1174
|
|
|
903
1175
|
# Upload without connecting data to a certain API
|
|
1176
|
+
client = ServiceClient(base_url=schemathesis_io_url, token=token)
|
|
1177
|
+
if experimental.SCHEMA_ANALYSIS.is_enabled and not client:
|
|
1178
|
+
from ..service.client import ServiceClient
|
|
1179
|
+
|
|
904
1180
|
client = ServiceClient(base_url=schemathesis_io_url, token=token)
|
|
905
1181
|
host_data = service.hosts.HostData(schemathesis_io_hostname, hosts_file)
|
|
906
1182
|
|
|
@@ -909,6 +1185,25 @@ def run(
|
|
|
909
1185
|
else:
|
|
910
1186
|
selected_checks = tuple(check for check in checks_module.ALL_CHECKS if check.__name__ in checks)
|
|
911
1187
|
|
|
1188
|
+
checks_config = CheckConfig()
|
|
1189
|
+
if experimental.POSITIVE_DATA_ACCEPTANCE.is_enabled:
|
|
1190
|
+
from ..specs.openapi.checks import positive_data_acceptance
|
|
1191
|
+
|
|
1192
|
+
selected_checks += (positive_data_acceptance,)
|
|
1193
|
+
if positive_data_acceptance_allowed_statuses:
|
|
1194
|
+
checks_config.positive_data_acceptance.allowed_statuses = positive_data_acceptance_allowed_statuses
|
|
1195
|
+
if missing_required_header_allowed_statuses:
|
|
1196
|
+
from ..specs.openapi.checks import missing_required_header
|
|
1197
|
+
|
|
1198
|
+
selected_checks += (missing_required_header,)
|
|
1199
|
+
checks_config.missing_required_header.allowed_statuses = missing_required_header_allowed_statuses
|
|
1200
|
+
if negative_data_rejection_allowed_statuses:
|
|
1201
|
+
checks_config.negative_data_rejection.allowed_statuses = negative_data_rejection_allowed_statuses
|
|
1202
|
+
if experimental.COVERAGE_PHASE.is_enabled:
|
|
1203
|
+
from ..specs.openapi.checks import unsupported_method
|
|
1204
|
+
|
|
1205
|
+
selected_checks += (unsupported_method,)
|
|
1206
|
+
|
|
912
1207
|
selected_checks = tuple(check for check in selected_checks if check.__name__ not in exclude_checks)
|
|
913
1208
|
|
|
914
1209
|
if fixups:
|
|
@@ -917,8 +1212,6 @@ def run(
|
|
|
917
1212
|
else:
|
|
918
1213
|
_fixups.install(fixups)
|
|
919
1214
|
|
|
920
|
-
if contrib_unique_data:
|
|
921
|
-
contrib.unique_data.install()
|
|
922
1215
|
if contrib_openapi_formats_uuid:
|
|
923
1216
|
contrib.openapi.formats.uuid.install()
|
|
924
1217
|
if contrib_openapi_fill_missing_examples:
|
|
@@ -940,7 +1233,6 @@ def run(
|
|
|
940
1233
|
base_url=base_url,
|
|
941
1234
|
started_at=started_at,
|
|
942
1235
|
validate_schema=validate_schema,
|
|
943
|
-
skip_deprecated_operations=skip_deprecated_operations,
|
|
944
1236
|
data_generation_methods=data_generation_methods,
|
|
945
1237
|
force_schema_version=force_schema_version,
|
|
946
1238
|
request_tls_verify=request_tls_verify,
|
|
@@ -951,14 +1243,12 @@ def run(
|
|
|
951
1243
|
auth_type=auth_type,
|
|
952
1244
|
override=override,
|
|
953
1245
|
headers=headers,
|
|
954
|
-
endpoint=endpoints or None,
|
|
955
|
-
method=methods or None,
|
|
956
|
-
tag=tags or None,
|
|
957
|
-
operation_id=operation_ids or None,
|
|
958
1246
|
request_timeout=request_timeout,
|
|
959
1247
|
seed=hypothesis_seed,
|
|
960
1248
|
exit_first=exit_first,
|
|
1249
|
+
no_failfast=no_failfast,
|
|
961
1250
|
max_failures=max_failures,
|
|
1251
|
+
unique_data=contrib_unique_data,
|
|
962
1252
|
dry_run=dry_run,
|
|
963
1253
|
store_interactions=cassette_path is not None,
|
|
964
1254
|
checks=selected_checks,
|
|
@@ -970,9 +1260,14 @@ def run(
|
|
|
970
1260
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
971
1261
|
hypothesis_settings=hypothesis_settings,
|
|
972
1262
|
generation_config=generation_config,
|
|
1263
|
+
checks_config=checks_config,
|
|
1264
|
+
output_config=output_config,
|
|
1265
|
+
service_client=client,
|
|
1266
|
+
filter_set=filter_set,
|
|
973
1267
|
)
|
|
974
1268
|
execute(
|
|
975
1269
|
event_stream,
|
|
1270
|
+
ctx=ctx,
|
|
976
1271
|
hypothesis_settings=hypothesis_settings,
|
|
977
1272
|
workers_num=workers_num,
|
|
978
1273
|
rate_limit=rate_limit,
|
|
@@ -980,6 +1275,7 @@ def run(
|
|
|
980
1275
|
wait_for_schema=wait_for_schema,
|
|
981
1276
|
validate_schema=validate_schema,
|
|
982
1277
|
cassette_path=cassette_path,
|
|
1278
|
+
cassette_format=cassette_format,
|
|
983
1279
|
cassette_preserve_exact_body_bytes=cassette_preserve_exact_body_bytes,
|
|
984
1280
|
junit_xml=junit_xml,
|
|
985
1281
|
verbosity=verbosity,
|
|
@@ -995,9 +1291,25 @@ def run(
|
|
|
995
1291
|
location=schema,
|
|
996
1292
|
base_url=base_url,
|
|
997
1293
|
started_at=started_at,
|
|
1294
|
+
output_config=output_config,
|
|
998
1295
|
)
|
|
999
1296
|
|
|
1000
1297
|
|
|
1298
|
+
def _ensure_unique_filter(values: Sequence[str], arg_name: str) -> None:
|
|
1299
|
+
if len(values) != len(set(values)):
|
|
1300
|
+
duplicates = ",".join(sorted({value for value in values if values.count(value) > 1}))
|
|
1301
|
+
raise click.UsageError(f"Duplicate values are not allowed for `{arg_name}`: {duplicates}")
|
|
1302
|
+
|
|
1303
|
+
|
|
1304
|
+
def _filter_by_expression_to_func(value: str | None, arg_name: str) -> Callable | None:
|
|
1305
|
+
if value:
|
|
1306
|
+
try:
|
|
1307
|
+
return expression_to_filter_function(value)
|
|
1308
|
+
except ValueError:
|
|
1309
|
+
raise click.UsageError(f"Invalid expression for {arg_name}: {value}") from None
|
|
1310
|
+
return None
|
|
1311
|
+
|
|
1312
|
+
|
|
1001
1313
|
def prepare_request_cert(cert: str | None, key: str | None) -> RequestCert | None:
|
|
1002
1314
|
if cert is not None and key is not None:
|
|
1003
1315
|
return cert, key
|
|
@@ -1015,7 +1327,6 @@ class LoaderConfig:
|
|
|
1015
1327
|
app: Any
|
|
1016
1328
|
base_url: str | None
|
|
1017
1329
|
validate_schema: bool
|
|
1018
|
-
skip_deprecated_operations: bool
|
|
1019
1330
|
data_generation_methods: tuple[DataGenerationMethod, ...]
|
|
1020
1331
|
force_schema_version: str | None
|
|
1021
1332
|
request_tls_verify: bool | str
|
|
@@ -1023,15 +1334,12 @@ class LoaderConfig:
|
|
|
1023
1334
|
request_cert: RequestCert | None
|
|
1024
1335
|
wait_for_schema: float | None
|
|
1025
1336
|
rate_limit: str | None
|
|
1337
|
+
output_config: OutputConfig
|
|
1338
|
+
generation_config: generation.GenerationConfig
|
|
1026
1339
|
# Network request parameters
|
|
1027
1340
|
auth: tuple[str, str] | None
|
|
1028
1341
|
auth_type: str | None
|
|
1029
1342
|
headers: dict[str, str] | None
|
|
1030
|
-
# Schema filters
|
|
1031
|
-
endpoint: Filter | None
|
|
1032
|
-
method: Filter | None
|
|
1033
|
-
tag: Filter | None
|
|
1034
|
-
operation_id: Filter | None
|
|
1035
1343
|
|
|
1036
1344
|
|
|
1037
1345
|
def into_event_stream(
|
|
@@ -1041,7 +1349,6 @@ def into_event_stream(
|
|
|
1041
1349
|
base_url: str | None,
|
|
1042
1350
|
started_at: str,
|
|
1043
1351
|
validate_schema: bool,
|
|
1044
|
-
skip_deprecated_operations: bool,
|
|
1045
1352
|
data_generation_methods: tuple[DataGenerationMethod, ...],
|
|
1046
1353
|
force_schema_version: str | None,
|
|
1047
1354
|
request_tls_verify: bool | str,
|
|
@@ -1054,26 +1361,27 @@ def into_event_stream(
|
|
|
1054
1361
|
headers: dict[str, str] | None,
|
|
1055
1362
|
request_timeout: int | None,
|
|
1056
1363
|
wait_for_schema: float | None,
|
|
1057
|
-
|
|
1058
|
-
endpoint: Filter | None,
|
|
1059
|
-
method: Filter | None,
|
|
1060
|
-
tag: Filter | None,
|
|
1061
|
-
operation_id: Filter | None,
|
|
1364
|
+
filter_set: FilterSet,
|
|
1062
1365
|
# Runtime behavior
|
|
1063
1366
|
checks: Iterable[CheckFunction],
|
|
1367
|
+
checks_config: CheckConfig,
|
|
1064
1368
|
max_response_time: int | None,
|
|
1065
1369
|
targets: Iterable[Target],
|
|
1066
1370
|
workers_num: int,
|
|
1067
1371
|
hypothesis_settings: hypothesis.settings | None,
|
|
1068
1372
|
generation_config: generation.GenerationConfig,
|
|
1373
|
+
output_config: OutputConfig,
|
|
1069
1374
|
seed: int | None,
|
|
1070
1375
|
exit_first: bool,
|
|
1376
|
+
no_failfast: bool,
|
|
1071
1377
|
max_failures: int | None,
|
|
1072
1378
|
rate_limit: str | None,
|
|
1379
|
+
unique_data: bool,
|
|
1073
1380
|
dry_run: bool,
|
|
1074
1381
|
store_interactions: bool,
|
|
1075
1382
|
stateful: Stateful | None,
|
|
1076
1383
|
stateful_recursion_limit: int,
|
|
1384
|
+
service_client: ServiceClient | None,
|
|
1077
1385
|
) -> Generator[events.ExecutionEvent, None, None]:
|
|
1078
1386
|
try:
|
|
1079
1387
|
if app is not None:
|
|
@@ -1083,7 +1391,6 @@ def into_event_stream(
|
|
|
1083
1391
|
app=app,
|
|
1084
1392
|
base_url=base_url,
|
|
1085
1393
|
validate_schema=validate_schema,
|
|
1086
|
-
skip_deprecated_operations=skip_deprecated_operations,
|
|
1087
1394
|
data_generation_methods=data_generation_methods,
|
|
1088
1395
|
force_schema_version=force_schema_version,
|
|
1089
1396
|
request_proxy=request_proxy,
|
|
@@ -1094,12 +1401,11 @@ def into_event_stream(
|
|
|
1094
1401
|
auth=auth,
|
|
1095
1402
|
auth_type=auth_type,
|
|
1096
1403
|
headers=headers,
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
tag=tag or None,
|
|
1100
|
-
operation_id=operation_id or None,
|
|
1404
|
+
output_config=output_config,
|
|
1405
|
+
generation_config=generation_config,
|
|
1101
1406
|
)
|
|
1102
1407
|
schema = load_schema(config)
|
|
1408
|
+
schema.filter_set = filter_set
|
|
1103
1409
|
yield from runner.from_schema(
|
|
1104
1410
|
schema,
|
|
1105
1411
|
auth=auth,
|
|
@@ -1112,11 +1418,14 @@ def into_event_stream(
|
|
|
1112
1418
|
request_cert=request_cert,
|
|
1113
1419
|
seed=seed,
|
|
1114
1420
|
exit_first=exit_first,
|
|
1421
|
+
no_failfast=no_failfast,
|
|
1115
1422
|
max_failures=max_failures,
|
|
1116
1423
|
started_at=started_at,
|
|
1424
|
+
unique_data=unique_data,
|
|
1117
1425
|
dry_run=dry_run,
|
|
1118
1426
|
store_interactions=store_interactions,
|
|
1119
1427
|
checks=checks,
|
|
1428
|
+
checks_config=checks_config,
|
|
1120
1429
|
max_response_time=max_response_time,
|
|
1121
1430
|
targets=targets,
|
|
1122
1431
|
workers_num=workers_num,
|
|
@@ -1126,13 +1435,17 @@ def into_event_stream(
|
|
|
1126
1435
|
generation_config=generation_config,
|
|
1127
1436
|
probe_config=probes.ProbeConfig(
|
|
1128
1437
|
base_url=config.base_url,
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1438
|
+
request=RequestConfig(
|
|
1439
|
+
timeout=request_timeout,
|
|
1440
|
+
tls_verify=config.request_tls_verify,
|
|
1441
|
+
proxy=config.request_proxy,
|
|
1442
|
+
cert=config.request_cert,
|
|
1443
|
+
),
|
|
1132
1444
|
auth=config.auth,
|
|
1133
1445
|
auth_type=config.auth_type,
|
|
1134
1446
|
headers=config.headers,
|
|
1135
1447
|
),
|
|
1448
|
+
service_client=service_client,
|
|
1136
1449
|
).execute()
|
|
1137
1450
|
except SchemaError as error:
|
|
1138
1451
|
yield events.InternalError.from_schema_error(error)
|
|
@@ -1233,15 +1546,12 @@ def get_loader_kwargs(loader: Callable, config: LoaderConfig) -> dict[str, Any]:
|
|
|
1233
1546
|
kwargs = {
|
|
1234
1547
|
"app": config.app,
|
|
1235
1548
|
"base_url": config.base_url,
|
|
1236
|
-
"method": config.method,
|
|
1237
|
-
"endpoint": config.endpoint,
|
|
1238
|
-
"tag": config.tag,
|
|
1239
|
-
"operation_id": config.operation_id,
|
|
1240
|
-
"skip_deprecated_operations": config.skip_deprecated_operations,
|
|
1241
1549
|
"validate_schema": config.validate_schema,
|
|
1242
1550
|
"force_schema_version": config.force_schema_version,
|
|
1243
1551
|
"data_generation_methods": config.data_generation_methods,
|
|
1244
1552
|
"rate_limit": config.rate_limit,
|
|
1553
|
+
"output_config": config.output_config,
|
|
1554
|
+
"generation_config": config.generation_config,
|
|
1245
1555
|
}
|
|
1246
1556
|
if loader not in (oas_loaders.from_path, oas_loaders.from_dict):
|
|
1247
1557
|
kwargs["headers"] = config.headers
|
|
@@ -1347,6 +1657,7 @@ class OutputStyle(Enum):
|
|
|
1347
1657
|
def execute(
|
|
1348
1658
|
event_stream: Generator[events.ExecutionEvent, None, None],
|
|
1349
1659
|
*,
|
|
1660
|
+
ctx: click.Context,
|
|
1350
1661
|
hypothesis_settings: hypothesis.settings,
|
|
1351
1662
|
workers_num: int,
|
|
1352
1663
|
rate_limit: str | None,
|
|
@@ -1354,6 +1665,7 @@ def execute(
|
|
|
1354
1665
|
wait_for_schema: float | None,
|
|
1355
1666
|
validate_schema: bool,
|
|
1356
1667
|
cassette_path: click.utils.LazyFile | None,
|
|
1668
|
+
cassette_format: cassettes.CassetteFormat,
|
|
1357
1669
|
cassette_preserve_exact_body_bytes: bool,
|
|
1358
1670
|
junit_xml: click.utils.LazyFile | None,
|
|
1359
1671
|
verbosity: int,
|
|
@@ -1369,6 +1681,7 @@ def execute(
|
|
|
1369
1681
|
location: str,
|
|
1370
1682
|
base_url: str | None,
|
|
1371
1683
|
started_at: str,
|
|
1684
|
+
output_config: OutputConfig,
|
|
1372
1685
|
) -> None:
|
|
1373
1686
|
"""Execute a prepared runner by drawing events from it and passing to a proper handler."""
|
|
1374
1687
|
handlers: list[EventHandler] = []
|
|
@@ -1415,8 +1728,12 @@ def execute(
|
|
|
1415
1728
|
# This handler should be first to have logs writing completed when the output handler will display statistic
|
|
1416
1729
|
_open_file(cassette_path)
|
|
1417
1730
|
handlers.append(
|
|
1418
|
-
cassettes.CassetteWriter(
|
|
1731
|
+
cassettes.CassetteWriter(
|
|
1732
|
+
cassette_path, format=cassette_format, preserve_exact_body_bytes=cassette_preserve_exact_body_bytes
|
|
1733
|
+
)
|
|
1419
1734
|
)
|
|
1735
|
+
for custom_handler in CUSTOM_HANDLERS:
|
|
1736
|
+
handlers.append(custom_handler(*ctx.args, **ctx.params))
|
|
1420
1737
|
handlers.append(get_output_handler(workers_num))
|
|
1421
1738
|
if sanitize_output:
|
|
1422
1739
|
handlers.insert(0, SanitizationHandler())
|
|
@@ -1432,6 +1749,7 @@ def execute(
|
|
|
1432
1749
|
verbosity=verbosity,
|
|
1433
1750
|
code_sample_style=code_sample_style,
|
|
1434
1751
|
report=report_context,
|
|
1752
|
+
output_config=output_config,
|
|
1435
1753
|
)
|
|
1436
1754
|
|
|
1437
1755
|
def shutdown() -> None:
|
|
@@ -1545,13 +1863,13 @@ def get_exit_code(event: events.ExecutionEvent) -> int:
|
|
|
1545
1863
|
|
|
1546
1864
|
@schemathesis.command(short_help="Replay requests from a saved cassette.")
|
|
1547
1865
|
@click.argument("cassette_path", type=click.Path(exists=True))
|
|
1548
|
-
@click.option("--id", "id_", help="ID of interaction to replay
|
|
1549
|
-
@click.option("--status", help="Status of interactions to replay
|
|
1550
|
-
@click.option("--uri", help="A regexp that filters interactions by their request URI
|
|
1551
|
-
@click.option("--method", help="A regexp that filters interactions by their request method
|
|
1552
|
-
@click.option("--no-color", help="Disable ANSI color escape codes
|
|
1553
|
-
@click.option("--force-color", help="Explicitly tells to enable ANSI color escape codes
|
|
1554
|
-
@click.option("--verbosity", "-v", help="Increase verbosity of the output
|
|
1866
|
+
@click.option("--id", "id_", help="ID of interaction to replay", type=str)
|
|
1867
|
+
@click.option("--status", help="Status of interactions to replay", type=str)
|
|
1868
|
+
@click.option("--uri", help="A regexp that filters interactions by their request URI", type=str)
|
|
1869
|
+
@click.option("--method", help="A regexp that filters interactions by their request method", type=str)
|
|
1870
|
+
@click.option("--no-color", help="Disable ANSI color escape codes", type=bool, is_flag=True)
|
|
1871
|
+
@click.option("--force-color", help="Explicitly tells to enable ANSI color escape codes", type=bool, is_flag=True)
|
|
1872
|
+
@click.option("--verbosity", "-v", help="Increase verbosity of the output", count=True)
|
|
1555
1873
|
@with_request_tls_verify
|
|
1556
1874
|
@with_request_proxy
|
|
1557
1875
|
@with_request_cert
|
|
@@ -1619,13 +1937,13 @@ def replay(
|
|
|
1619
1937
|
@click.argument("report", type=click.File(mode="rb"))
|
|
1620
1938
|
@click.option(
|
|
1621
1939
|
"--schemathesis-io-token",
|
|
1622
|
-
help="Schemathesis.io authentication token
|
|
1940
|
+
help="Schemathesis.io authentication token",
|
|
1623
1941
|
type=str,
|
|
1624
1942
|
envvar=service.TOKEN_ENV_VAR,
|
|
1625
1943
|
)
|
|
1626
1944
|
@click.option(
|
|
1627
1945
|
"--schemathesis-io-url",
|
|
1628
|
-
help="Schemathesis.io base URL
|
|
1946
|
+
help="Schemathesis.io base URL",
|
|
1629
1947
|
default=service.DEFAULT_URL,
|
|
1630
1948
|
type=str,
|
|
1631
1949
|
envvar=service.URL_ENV_VAR,
|
|
@@ -1747,6 +2065,39 @@ def decide_color_output(ctx: click.Context, no_color: bool, force_color: bool) -
|
|
|
1747
2065
|
ctx.color = False
|
|
1748
2066
|
|
|
1749
2067
|
|
|
2068
|
+
def add_option(*args: Any, cls: type = click.Option, **kwargs: Any) -> None:
|
|
2069
|
+
"""Add a new CLI option to `st run`."""
|
|
2070
|
+
run.params.append(cls(args, **kwargs))
|
|
2071
|
+
|
|
2072
|
+
|
|
2073
|
+
@dataclass
|
|
2074
|
+
class Group:
|
|
2075
|
+
name: str
|
|
2076
|
+
|
|
2077
|
+
def add_option(self, *args: Any, **kwargs: Any) -> None:
|
|
2078
|
+
kwargs["cls"] = GroupedOption
|
|
2079
|
+
kwargs["group"] = self.name
|
|
2080
|
+
add_option(*args, **kwargs)
|
|
2081
|
+
|
|
2082
|
+
|
|
2083
|
+
def add_group(name: str, *, index: int | None = None) -> Group:
|
|
2084
|
+
"""Add a custom options group to `st run`."""
|
|
2085
|
+
if index is not None:
|
|
2086
|
+
GROUPS.insert(index, name)
|
|
2087
|
+
else:
|
|
2088
|
+
GROUPS.append(name)
|
|
2089
|
+
return Group(name)
|
|
2090
|
+
|
|
2091
|
+
|
|
2092
|
+
def handler() -> Callable[[type], None]:
|
|
2093
|
+
"""Register a new CLI event handler."""
|
|
2094
|
+
|
|
2095
|
+
def _wrapper(cls: type) -> None:
|
|
2096
|
+
CUSTOM_HANDLERS.append(cls)
|
|
2097
|
+
|
|
2098
|
+
return _wrapper
|
|
2099
|
+
|
|
2100
|
+
|
|
1750
2101
|
@HookDispatcher.register_spec([HookScope.GLOBAL])
|
|
1751
2102
|
def after_init_cli_run_handlers(
|
|
1752
2103
|
context: HookContext, handlers: list[EventHandler], execution_context: ExecutionContext
|
|
@@ -1761,6 +2112,6 @@ def after_init_cli_run_handlers(
|
|
|
1761
2112
|
def process_call_kwargs(context: HookContext, case: Case, kwargs: dict[str, Any]) -> None:
|
|
1762
2113
|
"""Called before every network call in CLI tests.
|
|
1763
2114
|
|
|
1764
|
-
Aims to modify the argument passed to `case.call
|
|
2115
|
+
Aims to modify the argument passed to `case.call`.
|
|
1765
2116
|
Note that you need to modify `kwargs` in-place.
|
|
1766
2117
|
"""
|