schemathesis 3.25.5__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 +793 -448
- 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 +24 -4
- 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 +286 -115
- schemathesis/cli/output/short.py +25 -6
- 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 +323 -213
- schemathesis/parameters.py +4 -0
- schemathesis/runner/__init__.py +72 -22
- schemathesis/runner/events.py +86 -6
- schemathesis/runner/impl/context.py +104 -0
- schemathesis/runner/impl/core.py +447 -187
- schemathesis/runner/impl/solo.py +19 -29
- schemathesis/runner/impl/threadpool.py +70 -79
- schemathesis/{cli → runner}/probes.py +37 -25
- schemathesis/runner/serialization.py +150 -17
- schemathesis/sanitization.py +5 -1
- schemathesis/schemas.py +170 -102
- schemathesis/serializers.py +17 -4
- 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 +60 -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 +79 -61
- 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 +143 -31
- 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 +368 -242
- 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.5.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.5.dist-info/METADATA +0 -356
- schemathesis-3.25.5.dist-info/RECORD +0 -134
- {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.25.5.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,67 +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 ..
|
|
44
|
-
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
|
+
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
|
-
from ..specs.openapi import formats
|
|
48
47
|
from ..stateful import Stateful
|
|
49
|
-
from ..
|
|
50
|
-
from ..
|
|
51
|
-
from
|
|
52
|
-
from
|
|
53
|
-
from . import callbacks, cassettes, output, probes
|
|
54
|
-
from .constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS
|
|
48
|
+
from ..transports import RequestConfig
|
|
49
|
+
from ..transports.auth import get_requests_auth
|
|
50
|
+
from . import callbacks, cassettes, output
|
|
51
|
+
from .constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS, HealthCheck, Phase, Verbosity
|
|
55
52
|
from .context import ExecutionContext, FileReportContext, ServiceReportContext
|
|
56
53
|
from .debug import DebugOutputHandler
|
|
54
|
+
from .handlers import EventHandler
|
|
57
55
|
from .junitxml import JunitXMLHandler
|
|
58
|
-
from .options import CsvChoice, CsvEnumChoice,
|
|
56
|
+
from .options import CsvChoice, CsvEnumChoice, CsvListChoice, CustomHelpMessageChoice, OptionalInt
|
|
59
57
|
from .sanitization import SanitizationHandler
|
|
60
58
|
|
|
61
59
|
if TYPE_CHECKING:
|
|
60
|
+
import io
|
|
61
|
+
|
|
62
62
|
import hypothesis
|
|
63
63
|
import requests
|
|
64
|
-
|
|
64
|
+
|
|
65
|
+
from ..models import Case, CheckFunction
|
|
65
66
|
from ..schemas import BaseSchema
|
|
67
|
+
from ..service.client import ServiceClient
|
|
66
68
|
from ..specs.graphql.schemas import GraphQLSchema
|
|
67
|
-
from
|
|
69
|
+
from ..targets import Target
|
|
70
|
+
from ..types import NotSet, PathLike, RequestCert
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
__all__ = [
|
|
74
|
+
"EventHandler",
|
|
75
|
+
]
|
|
68
76
|
|
|
69
77
|
|
|
70
78
|
def _get_callable_names(items: tuple[Callable, ...]) -> tuple[str, ...]:
|
|
71
79
|
return tuple(item.__name__ for item in items)
|
|
72
80
|
|
|
73
81
|
|
|
82
|
+
CUSTOM_HANDLERS: list[type[EventHandler]] = []
|
|
74
83
|
CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
|
|
75
84
|
|
|
76
85
|
DEFAULT_CHECKS_NAMES = _get_callable_names(checks_module.DEFAULT_CHECKS)
|
|
@@ -96,13 +105,6 @@ DEPRECATED_SHOW_ERROR_TRACEBACKS_OPTION_WARNING = (
|
|
|
96
105
|
"Warning: Option `--show-errors-tracebacks` is deprecated and will be removed in Schemathesis 4.0. "
|
|
97
106
|
"Use `--show-trace` instead"
|
|
98
107
|
)
|
|
99
|
-
DEPRECATED_CONTRIB_UNIQUE_DATA_OPTION_WARNING = (
|
|
100
|
-
"The `--contrib-unique-data` CLI option and the corresponding `schemathesis.contrib.unique_data` hook "
|
|
101
|
-
"are **DEPRECATED**. The concept of this feature does not fit the core principles of Hypothesis where "
|
|
102
|
-
"strategies are configurable on a per-example basis but this feature implies uniqueness across examples. "
|
|
103
|
-
"This leads to cryptic error messages about external state and flaky test runs, "
|
|
104
|
-
"therefore it will be removed in Schemathesis 4.0"
|
|
105
|
-
)
|
|
106
108
|
CASSETTES_PATH_INVALID_USAGE_MESSAGE = "Can't use `--store-network-log` and `--cassette-path` simultaneously"
|
|
107
109
|
COLOR_OPTIONS_INVALID_USAGE_MESSAGE = "Can't use `--no-color` and `--force-color` simultaneously"
|
|
108
110
|
PHASES_INVALID_USAGE_MESSAGE = "Can't use `--hypothesis-phases` and `--hypothesis-no-phases` simultaneously"
|
|
@@ -112,21 +114,21 @@ def reset_checks() -> None:
|
|
|
112
114
|
"""Get checks list to their default state."""
|
|
113
115
|
# Useful in tests
|
|
114
116
|
checks_module.ALL_CHECKS = checks_module.DEFAULT_CHECKS + checks_module.OPTIONAL_CHECKS
|
|
115
|
-
CHECKS_TYPE.choices = _get_callable_names(checks_module.ALL_CHECKS)
|
|
117
|
+
CHECKS_TYPE.choices = (*_get_callable_names(checks_module.ALL_CHECKS), "all")
|
|
116
118
|
|
|
117
119
|
|
|
118
120
|
def reset_targets() -> None:
|
|
119
121
|
"""Get targets list to their default state."""
|
|
120
122
|
# Useful in tests
|
|
121
123
|
targets_module.ALL_TARGETS = targets_module.DEFAULT_TARGETS + targets_module.OPTIONAL_TARGETS
|
|
122
|
-
TARGETS_TYPE.choices = _get_callable_names(targets_module.ALL_TARGETS)
|
|
124
|
+
TARGETS_TYPE.choices = (*_get_callable_names(targets_module.ALL_TARGETS), "all")
|
|
123
125
|
|
|
124
126
|
|
|
125
127
|
@click.group(context_settings=CONTEXT_SETTINGS)
|
|
126
|
-
@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)
|
|
127
129
|
@click.version_option()
|
|
128
130
|
def schemathesis(pre_run: str | None = None) -> None:
|
|
129
|
-
"""
|
|
131
|
+
"""Property-based API testing for OpenAPI and GraphQL."""
|
|
130
132
|
# Don't use `envvar=HOOKS_MODULE_ENV_VAR` arg to raise a deprecation warning for hooks
|
|
131
133
|
hooks: str | None
|
|
132
134
|
if pre_run:
|
|
@@ -138,76 +140,89 @@ def schemathesis(pre_run: str | None = None) -> None:
|
|
|
138
140
|
load_hook(hooks)
|
|
139
141
|
|
|
140
142
|
|
|
141
|
-
|
|
142
|
-
filtering = "Testing scope", "Customize the scope of the API testing."
|
|
143
|
-
validation = "Response & Schema validation", "These options specify how API responses and schemas are validated."
|
|
144
|
-
hypothesis = "Hypothesis engine", "Configuration of the underlying Hypothesis engine."
|
|
145
|
-
generic = "Generic", None
|
|
143
|
+
GROUPS: list[str] = []
|
|
146
144
|
|
|
147
145
|
|
|
148
|
-
class
|
|
146
|
+
class CommandWithGroupedOptions(click.Command):
|
|
149
147
|
def format_options(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
|
150
|
-
# Group options first
|
|
151
148
|
groups = defaultdict(list)
|
|
152
149
|
for param in self.get_params(ctx):
|
|
153
150
|
rv = param.get_help_record(ctx)
|
|
154
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
|
+
|
|
155
159
|
if isinstance(param, GroupedOption):
|
|
156
160
|
group = param.group
|
|
157
161
|
else:
|
|
158
|
-
group =
|
|
159
|
-
groups[group].append(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
title, description = group.value
|
|
164
|
-
with formatter.section(title):
|
|
165
|
-
if description:
|
|
166
|
-
formatter.write_paragraph()
|
|
167
|
-
formatter.write_text(description)
|
|
168
|
-
formatter.write_paragraph()
|
|
169
|
-
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)
|
|
170
167
|
|
|
171
168
|
|
|
172
169
|
class GroupedOption(click.Option):
|
|
173
|
-
def __init__(self, *args: Any, group:
|
|
170
|
+
def __init__(self, *args: Any, group: str | None = None, **kwargs: Any):
|
|
174
171
|
super().__init__(*args, **kwargs)
|
|
175
172
|
self.group = group
|
|
176
173
|
|
|
177
174
|
|
|
178
|
-
|
|
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(
|
|
179
194
|
"--request-proxy",
|
|
180
|
-
help="Set the proxy for all network requests
|
|
195
|
+
help="Set the proxy for all network requests",
|
|
181
196
|
type=str,
|
|
182
197
|
)
|
|
183
|
-
with_request_tls_verify =
|
|
198
|
+
with_request_tls_verify = grouped_option(
|
|
184
199
|
"--request-tls-verify",
|
|
185
|
-
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",
|
|
186
201
|
type=str,
|
|
187
202
|
default="true",
|
|
188
203
|
show_default=True,
|
|
189
204
|
callback=callbacks.convert_boolean_string,
|
|
190
205
|
)
|
|
191
|
-
with_request_cert =
|
|
206
|
+
with_request_cert = grouped_option(
|
|
192
207
|
"--request-cert",
|
|
193
208
|
help="File path of unencrypted client certificate for authentication. "
|
|
194
209
|
"The certificate can be bundled with a private key (e.g. PEM) or the private "
|
|
195
|
-
"key can be provided with the --request-cert-key argument
|
|
210
|
+
"key can be provided with the --request-cert-key argument",
|
|
196
211
|
type=click.Path(exists=True),
|
|
197
212
|
default=None,
|
|
198
213
|
show_default=False,
|
|
199
214
|
)
|
|
200
|
-
with_request_cert_key =
|
|
215
|
+
with_request_cert_key = grouped_option(
|
|
201
216
|
"--request-cert-key",
|
|
202
|
-
help="
|
|
217
|
+
help="Specify the file path of the private key for the client certificate",
|
|
203
218
|
type=click.Path(exists=True),
|
|
204
219
|
default=None,
|
|
205
220
|
show_default=False,
|
|
206
221
|
callback=callbacks.validate_request_cert_key,
|
|
207
222
|
)
|
|
208
|
-
with_hosts_file =
|
|
223
|
+
with_hosts_file = grouped_option(
|
|
209
224
|
"--hosts-file",
|
|
210
|
-
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",
|
|
211
226
|
type=click.Path(dir_okay=False, writable=True),
|
|
212
227
|
default=service.DEFAULT_HOSTS_PATH,
|
|
213
228
|
envvar=service.HOSTS_PATH_ENV_VAR,
|
|
@@ -215,6 +230,37 @@ with_hosts_file = click.option(
|
|
|
215
230
|
)
|
|
216
231
|
|
|
217
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
|
+
|
|
218
264
|
class ReportToService:
|
|
219
265
|
pass
|
|
220
266
|
|
|
@@ -222,502 +268,587 @@ class ReportToService:
|
|
|
222
268
|
REPORT_TO_SERVICE = ReportToService()
|
|
223
269
|
|
|
224
270
|
|
|
225
|
-
@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
|
+
)
|
|
226
276
|
@click.argument("schema", type=str)
|
|
227
277
|
@click.argument("api_name", type=str, required=False, envvar=API_NAME_ENV_VAR)
|
|
228
|
-
@
|
|
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(
|
|
229
364
|
"--checks",
|
|
230
365
|
"-c",
|
|
231
366
|
multiple=True,
|
|
232
|
-
help="
|
|
233
|
-
"Provide a comma-separated list of checks such as 'not_a_server_error,status_code_conformance', etc. "
|
|
234
|
-
f"Default is '{','.join(DEFAULT_CHECKS_NAMES)}'.",
|
|
367
|
+
help="Comma-separated list of checks to run against API responses",
|
|
235
368
|
type=CHECKS_TYPE,
|
|
236
369
|
default=DEFAULT_CHECKS_NAMES,
|
|
237
|
-
cls=GroupedOption,
|
|
238
|
-
group=ParameterGroup.validation,
|
|
239
370
|
callback=callbacks.convert_checks,
|
|
240
371
|
show_default=True,
|
|
372
|
+
metavar="",
|
|
241
373
|
)
|
|
242
|
-
@
|
|
374
|
+
@grouped_option(
|
|
243
375
|
"--exclude-checks",
|
|
244
376
|
multiple=True,
|
|
245
|
-
help="
|
|
246
|
-
"Provide a comma-separated list of checks you wish to bypass.",
|
|
377
|
+
help="Comma-separated list of checks to skip during testing",
|
|
247
378
|
type=EXCLUDE_CHECKS_TYPE,
|
|
248
379
|
default=[],
|
|
249
|
-
cls=GroupedOption,
|
|
250
|
-
group=ParameterGroup.validation,
|
|
251
380
|
callback=callbacks.convert_checks,
|
|
252
381
|
show_default=True,
|
|
382
|
+
metavar="",
|
|
253
383
|
)
|
|
254
|
-
@
|
|
255
|
-
"--data-generation-method",
|
|
256
|
-
"-D",
|
|
257
|
-
"data_generation_methods",
|
|
258
|
-
help="Specifies the approach Schemathesis uses to generate test data. "
|
|
259
|
-
"Use 'positive' for valid data, 'negative' for invalid data, or 'all' for both. "
|
|
260
|
-
"Default is 'positive'.",
|
|
261
|
-
type=DATA_GENERATION_METHOD_TYPE,
|
|
262
|
-
default=DataGenerationMethod.default().name,
|
|
263
|
-
callback=callbacks.convert_data_generation_method,
|
|
264
|
-
show_default=True,
|
|
265
|
-
)
|
|
266
|
-
@click.option(
|
|
384
|
+
@grouped_option(
|
|
267
385
|
"--max-response-time",
|
|
268
|
-
help="
|
|
269
|
-
"The test will fail if a response time exceeds this limit. "
|
|
270
|
-
"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. ",
|
|
271
388
|
type=click.IntRange(min=1),
|
|
272
|
-
cls=GroupedOption,
|
|
273
|
-
group=ParameterGroup.validation,
|
|
274
|
-
)
|
|
275
|
-
@click.option(
|
|
276
|
-
"--target",
|
|
277
|
-
"-t",
|
|
278
|
-
"targets",
|
|
279
|
-
multiple=True,
|
|
280
|
-
help="Guides input generation to values more likely to expose bugs via targeted property-based testing.",
|
|
281
|
-
type=TARGETS_TYPE,
|
|
282
|
-
default=DEFAULT_TARGETS_NAMES,
|
|
283
|
-
show_default=True,
|
|
284
389
|
)
|
|
285
|
-
@
|
|
390
|
+
@grouped_option(
|
|
286
391
|
"-x",
|
|
287
392
|
"--exitfirst",
|
|
288
393
|
"exit_first",
|
|
289
394
|
is_flag=True,
|
|
290
395
|
default=False,
|
|
291
|
-
help="
|
|
396
|
+
help="Terminate the test suite immediately upon the first failure or error encountered",
|
|
292
397
|
show_default=True,
|
|
293
398
|
)
|
|
294
|
-
@
|
|
399
|
+
@grouped_option(
|
|
295
400
|
"--max-failures",
|
|
296
401
|
"max_failures",
|
|
297
402
|
type=click.IntRange(min=1),
|
|
298
|
-
help="
|
|
403
|
+
help="Terminate the test suite after reaching a specified number of failures or errors",
|
|
299
404
|
show_default=True,
|
|
300
405
|
)
|
|
301
|
-
@
|
|
302
|
-
|
|
303
|
-
"
|
|
304
|
-
|
|
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,
|
|
305
424
|
default=False,
|
|
306
|
-
|
|
425
|
+
show_default=True,
|
|
307
426
|
)
|
|
308
|
-
@
|
|
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(
|
|
309
463
|
"--auth",
|
|
310
464
|
"-a",
|
|
311
|
-
help="
|
|
465
|
+
help="Provide the server authentication details in the 'USER:PASSWORD' format",
|
|
312
466
|
type=str,
|
|
313
467
|
callback=callbacks.validate_auth,
|
|
314
468
|
)
|
|
315
|
-
@
|
|
469
|
+
@grouped_option(
|
|
316
470
|
"--auth-type",
|
|
317
471
|
"-A",
|
|
318
472
|
type=click.Choice(["basic", "digest"], case_sensitive=False),
|
|
319
473
|
default="basic",
|
|
320
|
-
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",
|
|
321
475
|
show_default=True,
|
|
476
|
+
metavar="",
|
|
322
477
|
)
|
|
323
|
-
@
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
type=str,
|
|
329
|
-
callback=callbacks.validate_set_query,
|
|
330
|
-
)
|
|
331
|
-
@click.option(
|
|
332
|
-
"--set-header",
|
|
333
|
-
"set_header",
|
|
334
|
-
help=r"OpenAPI: Override a specific header parameter by specifying 'parameter=value'",
|
|
335
|
-
multiple=True,
|
|
336
|
-
type=str,
|
|
337
|
-
callback=callbacks.validate_set_header,
|
|
338
|
-
)
|
|
339
|
-
@click.option(
|
|
340
|
-
"--set-cookie",
|
|
341
|
-
"set_cookie",
|
|
342
|
-
help=r"OpenAPI: Override a specific cookie parameter by specifying 'parameter=value'",
|
|
343
|
-
multiple=True,
|
|
478
|
+
@group("Filtering options")
|
|
479
|
+
@with_filters
|
|
480
|
+
@grouped_option(
|
|
481
|
+
"--include-by",
|
|
482
|
+
"include_by",
|
|
344
483
|
type=str,
|
|
345
|
-
|
|
484
|
+
help="Include API operations by expression",
|
|
346
485
|
)
|
|
347
|
-
@
|
|
348
|
-
"--
|
|
349
|
-
"
|
|
350
|
-
help=r"OpenAPI: Override a specific path parameter by specifying 'parameter=value'",
|
|
351
|
-
multiple=True,
|
|
486
|
+
@grouped_option(
|
|
487
|
+
"--exclude-by",
|
|
488
|
+
"exclude_by",
|
|
352
489
|
type=str,
|
|
353
|
-
|
|
490
|
+
help="Exclude API operations by expression",
|
|
354
491
|
)
|
|
355
|
-
@
|
|
356
|
-
"--
|
|
357
|
-
"
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
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,
|
|
363
499
|
)
|
|
364
|
-
@
|
|
500
|
+
@grouped_option(
|
|
365
501
|
"--endpoint",
|
|
366
502
|
"-E",
|
|
367
503
|
"endpoints",
|
|
368
504
|
type=str,
|
|
369
505
|
multiple=True,
|
|
370
|
-
help=r"API operation path pattern (e.g., users/\d+)
|
|
506
|
+
help=r"[DEPRECATED] API operation path pattern (e.g., users/\d+)",
|
|
371
507
|
callback=callbacks.validate_regex,
|
|
372
|
-
|
|
373
|
-
group=ParameterGroup.filtering,
|
|
508
|
+
hidden=True,
|
|
374
509
|
)
|
|
375
|
-
@
|
|
510
|
+
@grouped_option(
|
|
376
511
|
"--method",
|
|
377
512
|
"-M",
|
|
378
513
|
"methods",
|
|
379
514
|
type=str,
|
|
380
515
|
multiple=True,
|
|
381
|
-
help="HTTP method (e.g., GET, POST)
|
|
516
|
+
help="[DEPRECATED] HTTP method (e.g., GET, POST)",
|
|
382
517
|
callback=callbacks.validate_regex,
|
|
383
|
-
|
|
384
|
-
group=ParameterGroup.filtering,
|
|
518
|
+
hidden=True,
|
|
385
519
|
)
|
|
386
|
-
@
|
|
520
|
+
@grouped_option(
|
|
387
521
|
"--tag",
|
|
388
522
|
"-T",
|
|
389
523
|
"tags",
|
|
390
524
|
type=str,
|
|
391
525
|
multiple=True,
|
|
392
|
-
help="Schema tag pattern
|
|
526
|
+
help="[DEPRECATED] Schema tag pattern",
|
|
393
527
|
callback=callbacks.validate_regex,
|
|
394
|
-
|
|
395
|
-
group=ParameterGroup.filtering,
|
|
528
|
+
hidden=True,
|
|
396
529
|
)
|
|
397
|
-
@
|
|
530
|
+
@grouped_option(
|
|
398
531
|
"--operation-id",
|
|
399
532
|
"-O",
|
|
400
533
|
"operation_ids",
|
|
401
534
|
type=str,
|
|
402
535
|
multiple=True,
|
|
403
|
-
help="OpenAPI operationId pattern
|
|
536
|
+
help="[DEPRECATED] OpenAPI operationId pattern",
|
|
404
537
|
callback=callbacks.validate_regex,
|
|
405
|
-
|
|
406
|
-
group=ParameterGroup.filtering,
|
|
407
|
-
)
|
|
408
|
-
@click.option(
|
|
409
|
-
"--workers",
|
|
410
|
-
"-w",
|
|
411
|
-
"workers_num",
|
|
412
|
-
help="Sets the number of concurrent workers for testing. Auto-adjusts if 'auto' is specified.",
|
|
413
|
-
type=CustomHelpMessageChoice(
|
|
414
|
-
["auto"] + list(map(str, range(MIN_WORKERS, MAX_WORKERS + 1))),
|
|
415
|
-
choices_repr=f"[auto|{MIN_WORKERS}-{MAX_WORKERS}]",
|
|
416
|
-
),
|
|
417
|
-
default=str(DEFAULT_WORKERS),
|
|
418
|
-
show_default=True,
|
|
419
|
-
callback=callbacks.convert_workers,
|
|
420
|
-
)
|
|
421
|
-
@click.option(
|
|
422
|
-
"--base-url",
|
|
423
|
-
"-b",
|
|
424
|
-
help="Provides the base URL of the API, required when schema is provided as a file.",
|
|
425
|
-
type=str,
|
|
426
|
-
callback=callbacks.validate_base_url,
|
|
427
|
-
envvar=BASE_URL_ENV_VAR,
|
|
428
|
-
)
|
|
429
|
-
@click.option(
|
|
430
|
-
"--app",
|
|
431
|
-
help="Specifies the WSGI/ASGI application under test, provided as an importable Python path.",
|
|
432
|
-
type=str,
|
|
433
|
-
callback=callbacks.validate_app,
|
|
434
|
-
)
|
|
435
|
-
@click.option(
|
|
436
|
-
"--wait-for-schema",
|
|
437
|
-
help="Maximum duration, in seconds, to wait for the API schema to become available.",
|
|
438
|
-
type=click.FloatRange(1.0),
|
|
439
|
-
default=None,
|
|
440
|
-
envvar=WAIT_FOR_SCHEMA_ENV_VAR,
|
|
441
|
-
)
|
|
442
|
-
@click.option(
|
|
443
|
-
"--request-timeout",
|
|
444
|
-
help="Sets a timeout limit, in milliseconds, for each network request during tests.",
|
|
445
|
-
type=click.IntRange(1),
|
|
446
|
-
default=DEFAULT_RESPONSE_TIMEOUT,
|
|
447
|
-
)
|
|
448
|
-
@with_request_proxy
|
|
449
|
-
@with_request_tls_verify
|
|
450
|
-
@with_request_cert
|
|
451
|
-
@with_request_cert_key
|
|
452
|
-
@click.option(
|
|
453
|
-
"--validate-schema",
|
|
454
|
-
help="Toggles validation of incoming payloads against the defined API schema. "
|
|
455
|
-
"Set to 'True' to enable or 'False' to disable. "
|
|
456
|
-
"Default is 'False'.",
|
|
457
|
-
type=bool,
|
|
458
|
-
default=False,
|
|
459
|
-
show_default=True,
|
|
460
|
-
cls=GroupedOption,
|
|
461
|
-
group=ParameterGroup.validation,
|
|
538
|
+
hidden=True,
|
|
462
539
|
)
|
|
463
|
-
@
|
|
540
|
+
@grouped_option(
|
|
464
541
|
"--skip-deprecated-operations",
|
|
465
|
-
help="Exclude deprecated API operations from testing
|
|
542
|
+
help="[DEPRECATED] Exclude deprecated API operations from testing",
|
|
466
543
|
is_flag=True,
|
|
467
544
|
is_eager=True,
|
|
468
545
|
default=False,
|
|
469
546
|
show_default=True,
|
|
470
|
-
|
|
471
|
-
group=ParameterGroup.filtering,
|
|
547
|
+
hidden=True,
|
|
472
548
|
)
|
|
473
|
-
@
|
|
549
|
+
@group("Output options")
|
|
550
|
+
@grouped_option(
|
|
474
551
|
"--junit-xml",
|
|
475
|
-
help="
|
|
552
|
+
help="Output a JUnit-XML style report at the specified file path",
|
|
476
553
|
type=click.File("w", encoding="utf-8"),
|
|
477
554
|
)
|
|
478
|
-
@
|
|
479
|
-
"--
|
|
480
|
-
"
|
|
481
|
-
help="""Specifies how the generated report should be handled.
|
|
482
|
-
If used without an argument, the report data will automatically be uploaded to Schemathesis.io.
|
|
483
|
-
If a file name is provided, the report will be stored in that file.
|
|
484
|
-
The report data, consisting of a tar gz file with multiple JSON files, is subject to change.""",
|
|
485
|
-
is_flag=False,
|
|
486
|
-
flag_value="",
|
|
487
|
-
envvar=service.REPORT_ENV_VAR,
|
|
488
|
-
callback=callbacks.convert_report, # type: ignore
|
|
489
|
-
)
|
|
490
|
-
@click.option(
|
|
491
|
-
"--debug-output-file",
|
|
492
|
-
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",
|
|
493
558
|
type=click.File("w", encoding="utf-8"),
|
|
494
|
-
)
|
|
495
|
-
@click.option(
|
|
496
|
-
"--show-errors-tracebacks",
|
|
497
|
-
help="Displays complete traceback information for internal errors.",
|
|
498
|
-
is_flag=True,
|
|
499
559
|
is_eager=True,
|
|
500
|
-
default=False,
|
|
501
|
-
hidden=True,
|
|
502
|
-
show_default=True,
|
|
503
560
|
)
|
|
504
|
-
@
|
|
505
|
-
"--
|
|
506
|
-
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",
|
|
507
572
|
is_flag=True,
|
|
508
|
-
|
|
509
|
-
default=False,
|
|
510
|
-
show_default=True,
|
|
573
|
+
callback=callbacks.validate_preserve_exact_body_bytes,
|
|
511
574
|
)
|
|
512
|
-
@
|
|
575
|
+
@grouped_option(
|
|
513
576
|
"--code-sample-style",
|
|
514
|
-
help="
|
|
577
|
+
help="Code sample style for reproducing failures",
|
|
515
578
|
type=click.Choice([item.name for item in CodeSampleStyle]),
|
|
516
579
|
default=CodeSampleStyle.default().name,
|
|
517
580
|
callback=callbacks.convert_code_sample_style,
|
|
581
|
+
metavar="",
|
|
518
582
|
)
|
|
519
|
-
@
|
|
520
|
-
"--
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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",
|
|
524
589
|
)
|
|
525
|
-
@
|
|
526
|
-
"--
|
|
527
|
-
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",
|
|
528
601
|
is_flag=True,
|
|
529
|
-
|
|
602
|
+
is_eager=True,
|
|
603
|
+
default=False,
|
|
604
|
+
show_default=True,
|
|
530
605
|
)
|
|
531
|
-
@
|
|
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(
|
|
532
612
|
"--store-network-log",
|
|
533
|
-
help="
|
|
613
|
+
help="[DEPRECATED] Save the test outcomes in a VCR-compatible format",
|
|
534
614
|
type=click.File("w", encoding="utf-8"),
|
|
535
615
|
hidden=True,
|
|
536
616
|
)
|
|
537
|
-
@
|
|
538
|
-
"--
|
|
539
|
-
help="
|
|
540
|
-
|
|
541
|
-
|
|
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,
|
|
542
625
|
)
|
|
543
|
-
@
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
"
|
|
547
|
-
|
|
548
|
-
|
|
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="",
|
|
549
638
|
)
|
|
550
|
-
@
|
|
639
|
+
@grouped_option(
|
|
551
640
|
"--stateful",
|
|
552
|
-
help="
|
|
641
|
+
help="Enable or disable stateful testing",
|
|
553
642
|
type=click.Choice([item.name for item in Stateful]),
|
|
554
643
|
default=Stateful.links.name,
|
|
555
644
|
callback=callbacks.convert_stateful,
|
|
645
|
+
metavar="",
|
|
556
646
|
)
|
|
557
|
-
@
|
|
647
|
+
@grouped_option(
|
|
558
648
|
"--stateful-recursion-limit",
|
|
559
|
-
help="
|
|
649
|
+
help="Recursion depth limit for stateful testing",
|
|
560
650
|
default=DEFAULT_STATEFUL_RECURSION_LIMIT,
|
|
561
651
|
show_default=True,
|
|
562
652
|
type=click.IntRange(1, 100),
|
|
563
653
|
hidden=True,
|
|
564
654
|
)
|
|
565
|
-
@
|
|
566
|
-
"--
|
|
567
|
-
help="
|
|
568
|
-
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,
|
|
569
662
|
)
|
|
570
|
-
@
|
|
571
|
-
"--
|
|
572
|
-
|
|
573
|
-
|
|
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",
|
|
574
675
|
show_default=True,
|
|
575
|
-
|
|
676
|
+
callback=callbacks.convert_boolean_string,
|
|
576
677
|
)
|
|
577
|
-
@
|
|
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(
|
|
578
687
|
"--contrib-unique-data",
|
|
579
688
|
"contrib_unique_data",
|
|
580
|
-
help="
|
|
689
|
+
help="Force the generation of unique test cases",
|
|
581
690
|
is_flag=True,
|
|
582
691
|
default=False,
|
|
583
692
|
show_default=True,
|
|
584
693
|
)
|
|
585
|
-
@
|
|
694
|
+
@grouped_option(
|
|
586
695
|
"--contrib-openapi-formats-uuid",
|
|
587
696
|
"contrib_openapi_formats_uuid",
|
|
588
|
-
help="
|
|
697
|
+
help="Enable support for the 'uuid' string format in OpenAPI",
|
|
589
698
|
is_flag=True,
|
|
590
699
|
default=False,
|
|
591
700
|
show_default=True,
|
|
592
701
|
)
|
|
593
|
-
@
|
|
702
|
+
@grouped_option(
|
|
594
703
|
"--contrib-openapi-fill-missing-examples",
|
|
595
704
|
"contrib_openapi_fill_missing_examples",
|
|
596
|
-
help="
|
|
705
|
+
help="Enable generation of random examples for API operations that do not have explicit examples",
|
|
597
706
|
is_flag=True,
|
|
598
707
|
default=False,
|
|
599
708
|
show_default=True,
|
|
600
709
|
)
|
|
601
|
-
@
|
|
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(
|
|
602
762
|
"--hypothesis-database",
|
|
603
|
-
help="
|
|
763
|
+
help="Storage for examples discovered by Hypothesis. "
|
|
604
764
|
f"Use 'none' to disable, '{HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER}' for temporary storage, "
|
|
605
|
-
f"or specify a file path for persistent storage
|
|
765
|
+
f"or specify a file path for persistent storage",
|
|
606
766
|
type=str,
|
|
607
|
-
cls=GroupedOption,
|
|
608
|
-
group=ParameterGroup.hypothesis,
|
|
609
767
|
callback=callbacks.validate_hypothesis_database,
|
|
610
768
|
)
|
|
611
|
-
@
|
|
769
|
+
@grouped_option(
|
|
612
770
|
"--hypothesis-deadline",
|
|
613
|
-
help="
|
|
614
|
-
"Exceeding this limit will cause the test to fail
|
|
615
|
-
|
|
616
|
-
type=OptionalInt(1, 999999999 * 24 * 3600 * 1000),
|
|
617
|
-
cls=GroupedOption,
|
|
618
|
-
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),
|
|
619
774
|
)
|
|
620
|
-
@
|
|
775
|
+
@grouped_option(
|
|
621
776
|
"--hypothesis-derandomize",
|
|
622
|
-
help="Enables deterministic mode in Hypothesis, which eliminates random variation between
|
|
777
|
+
help="Enables deterministic mode in Hypothesis, which eliminates random variation between tests",
|
|
623
778
|
is_flag=True,
|
|
624
779
|
is_eager=True,
|
|
625
780
|
default=None,
|
|
626
781
|
show_default=True,
|
|
627
|
-
cls=GroupedOption,
|
|
628
|
-
group=ParameterGroup.hypothesis,
|
|
629
782
|
)
|
|
630
|
-
@
|
|
783
|
+
@grouped_option(
|
|
631
784
|
"--hypothesis-max-examples",
|
|
632
|
-
help="
|
|
785
|
+
help="The cap on the number of examples generated by Hypothesis for each API operation",
|
|
633
786
|
type=click.IntRange(1),
|
|
634
|
-
cls=GroupedOption,
|
|
635
|
-
group=ParameterGroup.hypothesis,
|
|
636
787
|
)
|
|
637
|
-
@
|
|
788
|
+
@grouped_option(
|
|
638
789
|
"--hypothesis-phases",
|
|
639
|
-
help="
|
|
790
|
+
help="Testing phases to execute",
|
|
640
791
|
type=CsvEnumChoice(Phase),
|
|
641
|
-
|
|
642
|
-
group=ParameterGroup.hypothesis,
|
|
792
|
+
metavar="",
|
|
643
793
|
)
|
|
644
|
-
@
|
|
794
|
+
@grouped_option(
|
|
645
795
|
"--hypothesis-no-phases",
|
|
646
|
-
help="
|
|
796
|
+
help="Testing phases to exclude from execution",
|
|
647
797
|
type=CsvEnumChoice(Phase),
|
|
648
|
-
|
|
649
|
-
group=ParameterGroup.hypothesis,
|
|
798
|
+
metavar="",
|
|
650
799
|
)
|
|
651
|
-
@
|
|
800
|
+
@grouped_option(
|
|
652
801
|
"--hypothesis-report-multiple-bugs",
|
|
653
|
-
help="
|
|
802
|
+
help="Report only the most easily reproducible error when multiple issues are found",
|
|
654
803
|
type=bool,
|
|
655
|
-
cls=GroupedOption,
|
|
656
|
-
group=ParameterGroup.hypothesis,
|
|
657
804
|
)
|
|
658
|
-
@
|
|
805
|
+
@grouped_option(
|
|
659
806
|
"--hypothesis-seed",
|
|
660
|
-
help="
|
|
807
|
+
help="Seed value for Hypothesis, ensuring reproducibility across test runs",
|
|
661
808
|
type=int,
|
|
662
|
-
cls=GroupedOption,
|
|
663
|
-
group=ParameterGroup.hypothesis,
|
|
664
809
|
)
|
|
665
|
-
@
|
|
810
|
+
@grouped_option(
|
|
666
811
|
"--hypothesis-suppress-health-check",
|
|
667
|
-
help="
|
|
668
|
-
"Provide a comma-separated list",
|
|
812
|
+
help="A comma-separated list of Hypothesis health checks to disable",
|
|
669
813
|
type=CsvEnumChoice(HealthCheck),
|
|
670
|
-
|
|
671
|
-
group=ParameterGroup.hypothesis,
|
|
814
|
+
metavar="",
|
|
672
815
|
)
|
|
673
|
-
@
|
|
816
|
+
@grouped_option(
|
|
674
817
|
"--hypothesis-verbosity",
|
|
675
|
-
help="
|
|
818
|
+
help="Verbosity level of Hypothesis output",
|
|
676
819
|
type=click.Choice([item.name for item in Verbosity]),
|
|
677
820
|
callback=callbacks.convert_verbosity,
|
|
678
|
-
|
|
679
|
-
group=ParameterGroup.hypothesis,
|
|
680
|
-
)
|
|
681
|
-
@click.option("--no-color", help="Disable ANSI color escape codes.", type=bool, is_flag=True)
|
|
682
|
-
@click.option("--force-color", help="Explicitly tells to enable ANSI color escape codes.", type=bool, is_flag=True)
|
|
683
|
-
@click.option(
|
|
684
|
-
"--experimental",
|
|
685
|
-
help="Enable experimental support for specific features.",
|
|
686
|
-
type=click.Choice([experimental.OPEN_API_3_1.name]),
|
|
687
|
-
callback=callbacks.convert_experimental,
|
|
688
|
-
multiple=True,
|
|
689
|
-
)
|
|
690
|
-
@click.option(
|
|
691
|
-
"--generation-allow-x00",
|
|
692
|
-
help="Determines whether to allow the generation of `\x00` bytes within strings.",
|
|
693
|
-
type=str,
|
|
694
|
-
default="true",
|
|
695
|
-
show_default=True,
|
|
696
|
-
callback=callbacks.convert_boolean_string,
|
|
821
|
+
metavar="",
|
|
697
822
|
)
|
|
698
|
-
@
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
|
704
835
|
)
|
|
705
|
-
@
|
|
836
|
+
@grouped_option(
|
|
706
837
|
"--schemathesis-io-token",
|
|
707
|
-
help="Schemathesis.io authentication token
|
|
838
|
+
help="Schemathesis.io authentication token",
|
|
708
839
|
type=str,
|
|
709
840
|
envvar=service.TOKEN_ENV_VAR,
|
|
710
841
|
)
|
|
711
|
-
@
|
|
842
|
+
@grouped_option(
|
|
712
843
|
"--schemathesis-io-url",
|
|
713
|
-
help="Schemathesis.io base URL
|
|
844
|
+
help="Schemathesis.io base URL",
|
|
714
845
|
default=service.DEFAULT_URL,
|
|
715
846
|
type=str,
|
|
716
847
|
envvar=service.URL_ENV_VAR,
|
|
717
848
|
)
|
|
718
|
-
@
|
|
849
|
+
@grouped_option(
|
|
719
850
|
"--schemathesis-io-telemetry",
|
|
720
|
-
help="
|
|
851
|
+
help="Whether to send anonymized usage data to Schemathesis.io along with your report",
|
|
721
852
|
type=str,
|
|
722
853
|
default="true",
|
|
723
854
|
show_default=True,
|
|
@@ -725,7 +856,10 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
|
|
|
725
856
|
envvar=service.TELEMETRY_ENV_VAR,
|
|
726
857
|
)
|
|
727
858
|
@with_hosts_file
|
|
728
|
-
@
|
|
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)
|
|
729
863
|
@click.pass_context
|
|
730
864
|
def run(
|
|
731
865
|
ctx: click.Context,
|
|
@@ -738,7 +872,11 @@ def run(
|
|
|
738
872
|
set_header: dict[str, str],
|
|
739
873
|
set_cookie: dict[str, str],
|
|
740
874
|
set_path: dict[str, str],
|
|
741
|
-
|
|
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],
|
|
742
880
|
checks: Iterable[str] = DEFAULT_CHECKS_NAMES,
|
|
743
881
|
exclude_checks: Iterable[str] = (),
|
|
744
882
|
data_generation_methods: tuple[DataGenerationMethod, ...] = DEFAULT_DATA_GENERATION_METHODS,
|
|
@@ -747,10 +885,33 @@ def run(
|
|
|
747
885
|
exit_first: bool = False,
|
|
748
886
|
max_failures: int | None = None,
|
|
749
887
|
dry_run: bool = False,
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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, ...] = (),
|
|
754
915
|
workers_num: int = DEFAULT_WORKERS,
|
|
755
916
|
base_url: str | None = None,
|
|
756
917
|
app: str | None = None,
|
|
@@ -767,6 +928,7 @@ def run(
|
|
|
767
928
|
show_trace: bool = False,
|
|
768
929
|
code_sample_style: CodeSampleStyle = CodeSampleStyle.default(),
|
|
769
930
|
cassette_path: click.utils.LazyFile | None = None,
|
|
931
|
+
cassette_format: cassettes.CassetteFormat = cassettes.CassetteFormat.VCR,
|
|
770
932
|
cassette_preserve_exact_body_bytes: bool = False,
|
|
771
933
|
store_network_log: click.utils.LazyFile | None = None,
|
|
772
934
|
wait_for_schema: float | None = None,
|
|
@@ -776,6 +938,7 @@ def run(
|
|
|
776
938
|
stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT,
|
|
777
939
|
force_schema_version: str | None = None,
|
|
778
940
|
sanitize_output: bool = True,
|
|
941
|
+
output_truncate: bool = True,
|
|
779
942
|
contrib_unique_data: bool = False,
|
|
780
943
|
contrib_openapi_formats_uuid: bool = False,
|
|
781
944
|
contrib_openapi_fill_missing_examples: bool = False,
|
|
@@ -793,18 +956,21 @@ def run(
|
|
|
793
956
|
no_color: bool = False,
|
|
794
957
|
report_value: str | None = None,
|
|
795
958
|
generation_allow_x00: bool = True,
|
|
959
|
+
generation_graphql_allow_null: bool = True,
|
|
960
|
+
generation_with_security_parameters: bool = True,
|
|
796
961
|
generation_codec: str = "utf-8",
|
|
797
962
|
schemathesis_io_token: str | None = None,
|
|
798
963
|
schemathesis_io_url: str = service.DEFAULT_URL,
|
|
799
964
|
schemathesis_io_telemetry: bool = True,
|
|
800
965
|
hosts_file: PathLike = service.DEFAULT_HOSTS_PATH,
|
|
801
966
|
force_color: bool = False,
|
|
967
|
+
**__kwargs,
|
|
802
968
|
) -> None:
|
|
803
969
|
"""Run tests against an API using a specified SCHEMA.
|
|
804
970
|
|
|
805
|
-
[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
|
|
806
972
|
|
|
807
|
-
[Optional] API_NAME: Identifier for uploading test data to Schemathesis.io
|
|
973
|
+
[Optional] API_NAME: Identifier for uploading test data to Schemathesis.io
|
|
808
974
|
"""
|
|
809
975
|
_hypothesis_phases: list[hypothesis.Phase] | None = None
|
|
810
976
|
if hypothesis_phases is not None:
|
|
@@ -816,23 +982,25 @@ def run(
|
|
|
816
982
|
_hypothesis_suppress_health_check: list[hypothesis.HealthCheck] | None = None
|
|
817
983
|
if hypothesis_suppress_health_check is not None:
|
|
818
984
|
_hypothesis_suppress_health_check = [
|
|
819
|
-
|
|
985
|
+
entry for health_check in hypothesis_suppress_health_check for entry in health_check.as_hypothesis()
|
|
820
986
|
]
|
|
821
987
|
|
|
822
|
-
if contrib_unique_data:
|
|
823
|
-
click.secho(DEPRECATED_CONTRIB_UNIQUE_DATA_OPTION_WARNING, fg="yellow")
|
|
824
|
-
|
|
825
988
|
if show_errors_tracebacks:
|
|
826
989
|
click.secho(DEPRECATED_SHOW_ERROR_TRACEBACKS_OPTION_WARNING, fg="yellow")
|
|
827
990
|
show_trace = show_errors_tracebacks
|
|
828
991
|
|
|
829
992
|
# Enable selected experiments
|
|
830
|
-
for experiment in
|
|
993
|
+
for experiment in experiments:
|
|
831
994
|
experiment.enable()
|
|
832
995
|
|
|
833
996
|
override = CaseOverride(query=set_query, headers=set_header, cookies=set_cookie, path_parameters=set_path)
|
|
834
997
|
|
|
835
|
-
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
|
+
)
|
|
836
1004
|
|
|
837
1005
|
report: ReportToService | click.utils.LazyFile | None
|
|
838
1006
|
if report_value is None:
|
|
@@ -856,6 +1024,109 @@ def run(
|
|
|
856
1024
|
click.secho(DEPRECATED_CASSETTE_PATH_OPTION_WARNING, fg="yellow")
|
|
857
1025
|
cassette_path = store_network_log
|
|
858
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
|
+
|
|
859
1130
|
schemathesis_io_hostname = urlparse(schemathesis_io_url).netloc
|
|
860
1131
|
token = schemathesis_io_token or service.hosts.get_token(hostname=schemathesis_io_hostname, hosts_file=hosts_file)
|
|
861
1132
|
schema_kind = callbacks.parse_schema_kind(schema, app)
|
|
@@ -902,6 +1173,10 @@ def run(
|
|
|
902
1173
|
from ..service.client import ServiceClient
|
|
903
1174
|
|
|
904
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
|
+
|
|
905
1180
|
client = ServiceClient(base_url=schemathesis_io_url, token=token)
|
|
906
1181
|
host_data = service.hosts.HostData(schemathesis_io_hostname, hosts_file)
|
|
907
1182
|
|
|
@@ -910,6 +1185,25 @@ def run(
|
|
|
910
1185
|
else:
|
|
911
1186
|
selected_checks = tuple(check for check in checks_module.ALL_CHECKS if check.__name__ in checks)
|
|
912
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
|
+
|
|
913
1207
|
selected_checks = tuple(check for check in selected_checks if check.__name__ not in exclude_checks)
|
|
914
1208
|
|
|
915
1209
|
if fixups:
|
|
@@ -918,8 +1212,6 @@ def run(
|
|
|
918
1212
|
else:
|
|
919
1213
|
_fixups.install(fixups)
|
|
920
1214
|
|
|
921
|
-
if contrib_unique_data:
|
|
922
|
-
contrib.unique_data.install()
|
|
923
1215
|
if contrib_openapi_formats_uuid:
|
|
924
1216
|
contrib.openapi.formats.uuid.install()
|
|
925
1217
|
if contrib_openapi_fill_missing_examples:
|
|
@@ -941,7 +1233,6 @@ def run(
|
|
|
941
1233
|
base_url=base_url,
|
|
942
1234
|
started_at=started_at,
|
|
943
1235
|
validate_schema=validate_schema,
|
|
944
|
-
skip_deprecated_operations=skip_deprecated_operations,
|
|
945
1236
|
data_generation_methods=data_generation_methods,
|
|
946
1237
|
force_schema_version=force_schema_version,
|
|
947
1238
|
request_tls_verify=request_tls_verify,
|
|
@@ -952,14 +1243,12 @@ def run(
|
|
|
952
1243
|
auth_type=auth_type,
|
|
953
1244
|
override=override,
|
|
954
1245
|
headers=headers,
|
|
955
|
-
endpoint=endpoints or None,
|
|
956
|
-
method=methods or None,
|
|
957
|
-
tag=tags or None,
|
|
958
|
-
operation_id=operation_ids or None,
|
|
959
1246
|
request_timeout=request_timeout,
|
|
960
1247
|
seed=hypothesis_seed,
|
|
961
1248
|
exit_first=exit_first,
|
|
1249
|
+
no_failfast=no_failfast,
|
|
962
1250
|
max_failures=max_failures,
|
|
1251
|
+
unique_data=contrib_unique_data,
|
|
963
1252
|
dry_run=dry_run,
|
|
964
1253
|
store_interactions=cassette_path is not None,
|
|
965
1254
|
checks=selected_checks,
|
|
@@ -971,9 +1260,14 @@ def run(
|
|
|
971
1260
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
972
1261
|
hypothesis_settings=hypothesis_settings,
|
|
973
1262
|
generation_config=generation_config,
|
|
1263
|
+
checks_config=checks_config,
|
|
1264
|
+
output_config=output_config,
|
|
1265
|
+
service_client=client,
|
|
1266
|
+
filter_set=filter_set,
|
|
974
1267
|
)
|
|
975
1268
|
execute(
|
|
976
1269
|
event_stream,
|
|
1270
|
+
ctx=ctx,
|
|
977
1271
|
hypothesis_settings=hypothesis_settings,
|
|
978
1272
|
workers_num=workers_num,
|
|
979
1273
|
rate_limit=rate_limit,
|
|
@@ -981,6 +1275,7 @@ def run(
|
|
|
981
1275
|
wait_for_schema=wait_for_schema,
|
|
982
1276
|
validate_schema=validate_schema,
|
|
983
1277
|
cassette_path=cassette_path,
|
|
1278
|
+
cassette_format=cassette_format,
|
|
984
1279
|
cassette_preserve_exact_body_bytes=cassette_preserve_exact_body_bytes,
|
|
985
1280
|
junit_xml=junit_xml,
|
|
986
1281
|
verbosity=verbosity,
|
|
@@ -996,9 +1291,25 @@ def run(
|
|
|
996
1291
|
location=schema,
|
|
997
1292
|
base_url=base_url,
|
|
998
1293
|
started_at=started_at,
|
|
1294
|
+
output_config=output_config,
|
|
999
1295
|
)
|
|
1000
1296
|
|
|
1001
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
|
+
|
|
1002
1313
|
def prepare_request_cert(cert: str | None, key: str | None) -> RequestCert | None:
|
|
1003
1314
|
if cert is not None and key is not None:
|
|
1004
1315
|
return cert, key
|
|
@@ -1016,7 +1327,6 @@ class LoaderConfig:
|
|
|
1016
1327
|
app: Any
|
|
1017
1328
|
base_url: str | None
|
|
1018
1329
|
validate_schema: bool
|
|
1019
|
-
skip_deprecated_operations: bool
|
|
1020
1330
|
data_generation_methods: tuple[DataGenerationMethod, ...]
|
|
1021
1331
|
force_schema_version: str | None
|
|
1022
1332
|
request_tls_verify: bool | str
|
|
@@ -1024,15 +1334,12 @@ class LoaderConfig:
|
|
|
1024
1334
|
request_cert: RequestCert | None
|
|
1025
1335
|
wait_for_schema: float | None
|
|
1026
1336
|
rate_limit: str | None
|
|
1337
|
+
output_config: OutputConfig
|
|
1338
|
+
generation_config: generation.GenerationConfig
|
|
1027
1339
|
# Network request parameters
|
|
1028
1340
|
auth: tuple[str, str] | None
|
|
1029
1341
|
auth_type: str | None
|
|
1030
1342
|
headers: dict[str, str] | None
|
|
1031
|
-
# Schema filters
|
|
1032
|
-
endpoint: Filter | None
|
|
1033
|
-
method: Filter | None
|
|
1034
|
-
tag: Filter | None
|
|
1035
|
-
operation_id: Filter | None
|
|
1036
1343
|
|
|
1037
1344
|
|
|
1038
1345
|
def into_event_stream(
|
|
@@ -1042,7 +1349,6 @@ def into_event_stream(
|
|
|
1042
1349
|
base_url: str | None,
|
|
1043
1350
|
started_at: str,
|
|
1044
1351
|
validate_schema: bool,
|
|
1045
|
-
skip_deprecated_operations: bool,
|
|
1046
1352
|
data_generation_methods: tuple[DataGenerationMethod, ...],
|
|
1047
1353
|
force_schema_version: str | None,
|
|
1048
1354
|
request_tls_verify: bool | str,
|
|
@@ -1055,26 +1361,27 @@ def into_event_stream(
|
|
|
1055
1361
|
headers: dict[str, str] | None,
|
|
1056
1362
|
request_timeout: int | None,
|
|
1057
1363
|
wait_for_schema: float | None,
|
|
1058
|
-
|
|
1059
|
-
endpoint: Filter | None,
|
|
1060
|
-
method: Filter | None,
|
|
1061
|
-
tag: Filter | None,
|
|
1062
|
-
operation_id: Filter | None,
|
|
1364
|
+
filter_set: FilterSet,
|
|
1063
1365
|
# Runtime behavior
|
|
1064
1366
|
checks: Iterable[CheckFunction],
|
|
1367
|
+
checks_config: CheckConfig,
|
|
1065
1368
|
max_response_time: int | None,
|
|
1066
1369
|
targets: Iterable[Target],
|
|
1067
1370
|
workers_num: int,
|
|
1068
1371
|
hypothesis_settings: hypothesis.settings | None,
|
|
1069
1372
|
generation_config: generation.GenerationConfig,
|
|
1373
|
+
output_config: OutputConfig,
|
|
1070
1374
|
seed: int | None,
|
|
1071
1375
|
exit_first: bool,
|
|
1376
|
+
no_failfast: bool,
|
|
1072
1377
|
max_failures: int | None,
|
|
1073
1378
|
rate_limit: str | None,
|
|
1379
|
+
unique_data: bool,
|
|
1074
1380
|
dry_run: bool,
|
|
1075
1381
|
store_interactions: bool,
|
|
1076
1382
|
stateful: Stateful | None,
|
|
1077
1383
|
stateful_recursion_limit: int,
|
|
1384
|
+
service_client: ServiceClient | None,
|
|
1078
1385
|
) -> Generator[events.ExecutionEvent, None, None]:
|
|
1079
1386
|
try:
|
|
1080
1387
|
if app is not None:
|
|
@@ -1084,7 +1391,6 @@ def into_event_stream(
|
|
|
1084
1391
|
app=app,
|
|
1085
1392
|
base_url=base_url,
|
|
1086
1393
|
validate_schema=validate_schema,
|
|
1087
|
-
skip_deprecated_operations=skip_deprecated_operations,
|
|
1088
1394
|
data_generation_methods=data_generation_methods,
|
|
1089
1395
|
force_schema_version=force_schema_version,
|
|
1090
1396
|
request_proxy=request_proxy,
|
|
@@ -1095,15 +1401,13 @@ def into_event_stream(
|
|
|
1095
1401
|
auth=auth,
|
|
1096
1402
|
auth_type=auth_type,
|
|
1097
1403
|
headers=headers,
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
tag=tag or None,
|
|
1101
|
-
operation_id=operation_id or None,
|
|
1404
|
+
output_config=output_config,
|
|
1405
|
+
generation_config=generation_config,
|
|
1102
1406
|
)
|
|
1103
|
-
|
|
1104
|
-
|
|
1407
|
+
schema = load_schema(config)
|
|
1408
|
+
schema.filter_set = filter_set
|
|
1105
1409
|
yield from runner.from_schema(
|
|
1106
|
-
|
|
1410
|
+
schema,
|
|
1107
1411
|
auth=auth,
|
|
1108
1412
|
auth_type=auth_type,
|
|
1109
1413
|
override=override,
|
|
@@ -1114,11 +1418,14 @@ def into_event_stream(
|
|
|
1114
1418
|
request_cert=request_cert,
|
|
1115
1419
|
seed=seed,
|
|
1116
1420
|
exit_first=exit_first,
|
|
1421
|
+
no_failfast=no_failfast,
|
|
1117
1422
|
max_failures=max_failures,
|
|
1118
1423
|
started_at=started_at,
|
|
1424
|
+
unique_data=unique_data,
|
|
1119
1425
|
dry_run=dry_run,
|
|
1120
1426
|
store_interactions=store_interactions,
|
|
1121
1427
|
checks=checks,
|
|
1428
|
+
checks_config=checks_config,
|
|
1122
1429
|
max_response_time=max_response_time,
|
|
1123
1430
|
targets=targets,
|
|
1124
1431
|
workers_num=workers_num,
|
|
@@ -1126,6 +1433,19 @@ def into_event_stream(
|
|
|
1126
1433
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
1127
1434
|
hypothesis_settings=hypothesis_settings,
|
|
1128
1435
|
generation_config=generation_config,
|
|
1436
|
+
probe_config=probes.ProbeConfig(
|
|
1437
|
+
base_url=config.base_url,
|
|
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
|
+
),
|
|
1444
|
+
auth=config.auth,
|
|
1445
|
+
auth_type=config.auth_type,
|
|
1446
|
+
headers=config.headers,
|
|
1447
|
+
),
|
|
1448
|
+
service_client=service_client,
|
|
1129
1449
|
).execute()
|
|
1130
1450
|
except SchemaError as error:
|
|
1131
1451
|
yield events.InternalError.from_schema_error(error)
|
|
@@ -1133,19 +1453,6 @@ def into_event_stream(
|
|
|
1133
1453
|
yield events.InternalError.from_exc(exc)
|
|
1134
1454
|
|
|
1135
1455
|
|
|
1136
|
-
def run_probes(schema: BaseSchema, config: LoaderConfig) -> None:
|
|
1137
|
-
"""Discover capabilities of the tested app."""
|
|
1138
|
-
probe_results = probes.run(schema, config)
|
|
1139
|
-
for result in probe_results:
|
|
1140
|
-
if isinstance(result.probe, probes.NullByteInHeader) and result.is_failure:
|
|
1141
|
-
from ..specs.openapi._hypothesis import HEADER_FORMAT, header_values
|
|
1142
|
-
|
|
1143
|
-
formats.register(
|
|
1144
|
-
HEADER_FORMAT,
|
|
1145
|
-
header_values(blacklist_characters="\n\r\x00").map(str.lstrip),
|
|
1146
|
-
)
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
1456
|
def load_schema(config: LoaderConfig) -> BaseSchema:
|
|
1150
1457
|
"""Automatically load API schema."""
|
|
1151
1458
|
first: Callable[[LoaderConfig], BaseSchema]
|
|
@@ -1239,15 +1546,12 @@ def get_loader_kwargs(loader: Callable, config: LoaderConfig) -> dict[str, Any]:
|
|
|
1239
1546
|
kwargs = {
|
|
1240
1547
|
"app": config.app,
|
|
1241
1548
|
"base_url": config.base_url,
|
|
1242
|
-
"method": config.method,
|
|
1243
|
-
"endpoint": config.endpoint,
|
|
1244
|
-
"tag": config.tag,
|
|
1245
|
-
"operation_id": config.operation_id,
|
|
1246
|
-
"skip_deprecated_operations": config.skip_deprecated_operations,
|
|
1247
1549
|
"validate_schema": config.validate_schema,
|
|
1248
1550
|
"force_schema_version": config.force_schema_version,
|
|
1249
1551
|
"data_generation_methods": config.data_generation_methods,
|
|
1250
1552
|
"rate_limit": config.rate_limit,
|
|
1553
|
+
"output_config": config.output_config,
|
|
1554
|
+
"generation_config": config.generation_config,
|
|
1251
1555
|
}
|
|
1252
1556
|
if loader not in (oas_loaders.from_path, oas_loaders.from_dict):
|
|
1253
1557
|
kwargs["headers"] = config.headers
|
|
@@ -1353,6 +1657,7 @@ class OutputStyle(Enum):
|
|
|
1353
1657
|
def execute(
|
|
1354
1658
|
event_stream: Generator[events.ExecutionEvent, None, None],
|
|
1355
1659
|
*,
|
|
1660
|
+
ctx: click.Context,
|
|
1356
1661
|
hypothesis_settings: hypothesis.settings,
|
|
1357
1662
|
workers_num: int,
|
|
1358
1663
|
rate_limit: str | None,
|
|
@@ -1360,6 +1665,7 @@ def execute(
|
|
|
1360
1665
|
wait_for_schema: float | None,
|
|
1361
1666
|
validate_schema: bool,
|
|
1362
1667
|
cassette_path: click.utils.LazyFile | None,
|
|
1668
|
+
cassette_format: cassettes.CassetteFormat,
|
|
1363
1669
|
cassette_preserve_exact_body_bytes: bool,
|
|
1364
1670
|
junit_xml: click.utils.LazyFile | None,
|
|
1365
1671
|
verbosity: int,
|
|
@@ -1375,6 +1681,7 @@ def execute(
|
|
|
1375
1681
|
location: str,
|
|
1376
1682
|
base_url: str | None,
|
|
1377
1683
|
started_at: str,
|
|
1684
|
+
output_config: OutputConfig,
|
|
1378
1685
|
) -> None:
|
|
1379
1686
|
"""Execute a prepared runner by drawing events from it and passing to a proper handler."""
|
|
1380
1687
|
handlers: list[EventHandler] = []
|
|
@@ -1421,8 +1728,12 @@ def execute(
|
|
|
1421
1728
|
# This handler should be first to have logs writing completed when the output handler will display statistic
|
|
1422
1729
|
_open_file(cassette_path)
|
|
1423
1730
|
handlers.append(
|
|
1424
|
-
cassettes.CassetteWriter(
|
|
1731
|
+
cassettes.CassetteWriter(
|
|
1732
|
+
cassette_path, format=cassette_format, preserve_exact_body_bytes=cassette_preserve_exact_body_bytes
|
|
1733
|
+
)
|
|
1425
1734
|
)
|
|
1735
|
+
for custom_handler in CUSTOM_HANDLERS:
|
|
1736
|
+
handlers.append(custom_handler(*ctx.args, **ctx.params))
|
|
1426
1737
|
handlers.append(get_output_handler(workers_num))
|
|
1427
1738
|
if sanitize_output:
|
|
1428
1739
|
handlers.insert(0, SanitizationHandler())
|
|
@@ -1438,6 +1749,7 @@ def execute(
|
|
|
1438
1749
|
verbosity=verbosity,
|
|
1439
1750
|
code_sample_style=code_sample_style,
|
|
1440
1751
|
report=report_context,
|
|
1752
|
+
output_config=output_config,
|
|
1441
1753
|
)
|
|
1442
1754
|
|
|
1443
1755
|
def shutdown() -> None:
|
|
@@ -1551,13 +1863,13 @@ def get_exit_code(event: events.ExecutionEvent) -> int:
|
|
|
1551
1863
|
|
|
1552
1864
|
@schemathesis.command(short_help="Replay requests from a saved cassette.")
|
|
1553
1865
|
@click.argument("cassette_path", type=click.Path(exists=True))
|
|
1554
|
-
@click.option("--id", "id_", help="ID of interaction to replay
|
|
1555
|
-
@click.option("--status", help="Status of interactions to replay
|
|
1556
|
-
@click.option("--uri", help="A regexp that filters interactions by their request URI
|
|
1557
|
-
@click.option("--method", help="A regexp that filters interactions by their request method
|
|
1558
|
-
@click.option("--no-color", help="Disable ANSI color escape codes
|
|
1559
|
-
@click.option("--force-color", help="Explicitly tells to enable ANSI color escape codes
|
|
1560
|
-
@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)
|
|
1561
1873
|
@with_request_tls_verify
|
|
1562
1874
|
@with_request_proxy
|
|
1563
1875
|
@with_request_cert
|
|
@@ -1625,13 +1937,13 @@ def replay(
|
|
|
1625
1937
|
@click.argument("report", type=click.File(mode="rb"))
|
|
1626
1938
|
@click.option(
|
|
1627
1939
|
"--schemathesis-io-token",
|
|
1628
|
-
help="Schemathesis.io authentication token
|
|
1940
|
+
help="Schemathesis.io authentication token",
|
|
1629
1941
|
type=str,
|
|
1630
1942
|
envvar=service.TOKEN_ENV_VAR,
|
|
1631
1943
|
)
|
|
1632
1944
|
@click.option(
|
|
1633
1945
|
"--schemathesis-io-url",
|
|
1634
|
-
help="Schemathesis.io base URL
|
|
1946
|
+
help="Schemathesis.io base URL",
|
|
1635
1947
|
default=service.DEFAULT_URL,
|
|
1636
1948
|
type=str,
|
|
1637
1949
|
envvar=service.URL_ENV_VAR,
|
|
@@ -1753,6 +2065,39 @@ def decide_color_output(ctx: click.Context, no_color: bool, force_color: bool) -
|
|
|
1753
2065
|
ctx.color = False
|
|
1754
2066
|
|
|
1755
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
|
+
|
|
1756
2101
|
@HookDispatcher.register_spec([HookScope.GLOBAL])
|
|
1757
2102
|
def after_init_cli_run_handlers(
|
|
1758
2103
|
context: HookContext, handlers: list[EventHandler], execution_context: ExecutionContext
|
|
@@ -1767,6 +2112,6 @@ def after_init_cli_run_handlers(
|
|
|
1767
2112
|
def process_call_kwargs(context: HookContext, case: Case, kwargs: dict[str, Any]) -> None:
|
|
1768
2113
|
"""Called before every network call in CLI tests.
|
|
1769
2114
|
|
|
1770
|
-
Aims to modify the argument passed to `case.call
|
|
2115
|
+
Aims to modify the argument passed to `case.call`.
|
|
1771
2116
|
Note that you need to modify `kwargs` in-place.
|
|
1772
2117
|
"""
|