schemathesis 3.25.5__py3-none-any.whl → 3.26.0__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/_dependency_versions.py +1 -0
- schemathesis/_hypothesis.py +1 -0
- schemathesis/_xml.py +1 -0
- schemathesis/auths.py +1 -0
- schemathesis/cli/__init__.py +39 -37
- schemathesis/cli/cassettes.py +4 -4
- schemathesis/cli/context.py +6 -0
- schemathesis/cli/output/default.py +185 -45
- schemathesis/cli/output/short.py +8 -0
- schemathesis/experimental/__init__.py +7 -0
- schemathesis/filters.py +1 -0
- schemathesis/models.py +5 -2
- schemathesis/parameters.py +1 -0
- schemathesis/runner/__init__.py +36 -9
- schemathesis/runner/events.py +33 -1
- schemathesis/runner/impl/core.py +99 -23
- schemathesis/{cli → runner}/probes.py +32 -21
- schemathesis/runner/serialization.py +4 -2
- schemathesis/schemas.py +1 -0
- schemathesis/serializers.py +11 -3
- schemathesis/service/client.py +35 -2
- schemathesis/service/extensions.py +224 -0
- schemathesis/service/hosts.py +1 -0
- schemathesis/service/metadata.py +24 -0
- schemathesis/service/models.py +210 -2
- schemathesis/service/serialization.py +44 -1
- schemathesis/specs/openapi/__init__.py +1 -0
- schemathesis/specs/openapi/_hypothesis.py +9 -1
- schemathesis/specs/openapi/examples.py +22 -24
- schemathesis/specs/openapi/expressions/__init__.py +1 -0
- schemathesis/specs/openapi/expressions/lexer.py +1 -0
- schemathesis/specs/openapi/expressions/nodes.py +1 -0
- schemathesis/specs/openapi/links.py +1 -0
- schemathesis/specs/openapi/media_types.py +34 -0
- schemathesis/specs/openapi/negative/mutations.py +1 -0
- schemathesis/specs/openapi/schemas.py +10 -3
- schemathesis/specs/openapi/security.py +5 -1
- {schemathesis-3.25.5.dist-info → schemathesis-3.26.0.dist-info}/METADATA +8 -5
- {schemathesis-3.25.5.dist-info → schemathesis-3.26.0.dist-info}/RECORD +42 -40
- {schemathesis-3.25.5.dist-info → schemathesis-3.26.0.dist-info}/WHEEL +0 -0
- {schemathesis-3.25.5.dist-info → schemathesis-3.26.0.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.25.5.dist-info → schemathesis-3.26.0.dist-info}/licenses/LICENSE +0 -0
schemathesis/_hypothesis.py
CHANGED
schemathesis/_xml.py
CHANGED
schemathesis/auths.py
CHANGED
schemathesis/cli/__init__.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import base64
|
|
3
4
|
import enum
|
|
4
5
|
import io
|
|
@@ -10,48 +11,45 @@ from collections import defaultdict
|
|
|
10
11
|
from dataclasses import dataclass
|
|
11
12
|
from enum import Enum
|
|
12
13
|
from queue import Queue
|
|
13
|
-
from typing import Any, Callable, Generator, Iterable, NoReturn, cast
|
|
14
|
+
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, NoReturn, cast
|
|
14
15
|
from urllib.parse import urlparse
|
|
15
16
|
|
|
16
17
|
import click
|
|
17
18
|
|
|
18
19
|
from .. import checks as checks_module
|
|
19
|
-
from .. import contrib, experimental, generation
|
|
20
|
+
from .. import contrib, experimental, generation, runner, service
|
|
20
21
|
from .. import fixups as _fixups
|
|
21
|
-
from .. import runner, service
|
|
22
22
|
from .. import targets as targets_module
|
|
23
|
+
from .._override import CaseOverride
|
|
23
24
|
from ..code_samples import CodeSampleStyle
|
|
24
|
-
from .constants import HealthCheck, Phase, Verbosity
|
|
25
|
-
from ..generation import DEFAULT_DATA_GENERATION_METHODS, DataGenerationMethod
|
|
26
25
|
from ..constants import (
|
|
27
26
|
API_NAME_ENV_VAR,
|
|
28
27
|
BASE_URL_ENV_VAR,
|
|
29
28
|
DEFAULT_RESPONSE_TIMEOUT,
|
|
30
29
|
DEFAULT_STATEFUL_RECURSION_LIMIT,
|
|
30
|
+
EXTENSIONS_DOCUMENTATION_URL,
|
|
31
31
|
HOOKS_MODULE_ENV_VAR,
|
|
32
32
|
HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER,
|
|
33
|
-
WAIT_FOR_SCHEMA_ENV_VAR,
|
|
34
|
-
EXTENSIONS_DOCUMENTATION_URL,
|
|
35
33
|
ISSUE_TRACKER_URL,
|
|
34
|
+
WAIT_FOR_SCHEMA_ENV_VAR,
|
|
36
35
|
)
|
|
37
|
-
from ..exceptions import SchemaError,
|
|
36
|
+
from ..exceptions import SchemaError, SchemaErrorType, extract_nth_traceback
|
|
38
37
|
from ..fixups import ALL_FIXUPS
|
|
39
|
-
from ..
|
|
40
|
-
from .._override import CaseOverride
|
|
41
|
-
from ..transports.auth import get_requests_auth
|
|
38
|
+
from ..generation import DEFAULT_DATA_GENERATION_METHODS, DataGenerationMethod
|
|
42
39
|
from ..hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookScope
|
|
40
|
+
from ..internal.datetime import current_datetime
|
|
41
|
+
from ..internal.validation import file_exists
|
|
42
|
+
from ..loaders import load_app, load_yaml
|
|
43
43
|
from ..models import Case, CheckFunction
|
|
44
|
-
from ..runner import events, prepare_hypothesis_settings
|
|
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
48
|
from ..targets import Target
|
|
49
|
+
from ..transports.auth import get_requests_auth
|
|
50
50
|
from ..types import Filter, PathLike, RequestCert
|
|
51
|
-
from
|
|
52
|
-
from
|
|
53
|
-
from . import callbacks, cassettes, output, probes
|
|
54
|
-
from .constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS
|
|
51
|
+
from . import callbacks, cassettes, output
|
|
52
|
+
from .constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS, HealthCheck, Phase, Verbosity
|
|
55
53
|
from .context import ExecutionContext, FileReportContext, ServiceReportContext
|
|
56
54
|
from .debug import DebugOutputHandler
|
|
57
55
|
from .junitxml import JunitXMLHandler
|
|
@@ -61,8 +59,9 @@ from .sanitization import SanitizationHandler
|
|
|
61
59
|
if TYPE_CHECKING:
|
|
62
60
|
import hypothesis
|
|
63
61
|
import requests
|
|
64
|
-
|
|
62
|
+
|
|
65
63
|
from ..schemas import BaseSchema
|
|
64
|
+
from ..service.client import ServiceClient
|
|
66
65
|
from ..specs.graphql.schemas import GraphQLSchema
|
|
67
66
|
from .handlers import EventHandler
|
|
68
67
|
|
|
@@ -682,8 +681,9 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
|
|
|
682
681
|
@click.option("--force-color", help="Explicitly tells to enable ANSI color escape codes.", type=bool, is_flag=True)
|
|
683
682
|
@click.option(
|
|
684
683
|
"--experimental",
|
|
684
|
+
"experiments",
|
|
685
685
|
help="Enable experimental support for specific features.",
|
|
686
|
-
type=click.Choice([experimental.OPEN_API_3_1.name]),
|
|
686
|
+
type=click.Choice([experimental.OPEN_API_3_1.name, experimental.SCHEMA_ANALYSIS.name]),
|
|
687
687
|
callback=callbacks.convert_experimental,
|
|
688
688
|
multiple=True,
|
|
689
689
|
)
|
|
@@ -738,7 +738,7 @@ def run(
|
|
|
738
738
|
set_header: dict[str, str],
|
|
739
739
|
set_cookie: dict[str, str],
|
|
740
740
|
set_path: dict[str, str],
|
|
741
|
-
|
|
741
|
+
experiments: list,
|
|
742
742
|
checks: Iterable[str] = DEFAULT_CHECKS_NAMES,
|
|
743
743
|
exclude_checks: Iterable[str] = (),
|
|
744
744
|
data_generation_methods: tuple[DataGenerationMethod, ...] = DEFAULT_DATA_GENERATION_METHODS,
|
|
@@ -827,7 +827,7 @@ def run(
|
|
|
827
827
|
show_trace = show_errors_tracebacks
|
|
828
828
|
|
|
829
829
|
# Enable selected experiments
|
|
830
|
-
for experiment in
|
|
830
|
+
for experiment in experiments:
|
|
831
831
|
experiment.enable()
|
|
832
832
|
|
|
833
833
|
override = CaseOverride(query=set_query, headers=set_header, cookies=set_cookie, path_parameters=set_path)
|
|
@@ -902,6 +902,10 @@ def run(
|
|
|
902
902
|
from ..service.client import ServiceClient
|
|
903
903
|
|
|
904
904
|
# Upload without connecting data to a certain API
|
|
905
|
+
client = ServiceClient(base_url=schemathesis_io_url, token=token)
|
|
906
|
+
if experimental.SCHEMA_ANALYSIS.is_enabled and not client:
|
|
907
|
+
from ..service.client import ServiceClient
|
|
908
|
+
|
|
905
909
|
client = ServiceClient(base_url=schemathesis_io_url, token=token)
|
|
906
910
|
host_data = service.hosts.HostData(schemathesis_io_hostname, hosts_file)
|
|
907
911
|
|
|
@@ -971,6 +975,7 @@ def run(
|
|
|
971
975
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
972
976
|
hypothesis_settings=hypothesis_settings,
|
|
973
977
|
generation_config=generation_config,
|
|
978
|
+
service_client=client,
|
|
974
979
|
)
|
|
975
980
|
execute(
|
|
976
981
|
event_stream,
|
|
@@ -1075,6 +1080,7 @@ def into_event_stream(
|
|
|
1075
1080
|
store_interactions: bool,
|
|
1076
1081
|
stateful: Stateful | None,
|
|
1077
1082
|
stateful_recursion_limit: int,
|
|
1083
|
+
service_client: ServiceClient | None,
|
|
1078
1084
|
) -> Generator[events.ExecutionEvent, None, None]:
|
|
1079
1085
|
try:
|
|
1080
1086
|
if app is not None:
|
|
@@ -1100,10 +1106,9 @@ def into_event_stream(
|
|
|
1100
1106
|
tag=tag or None,
|
|
1101
1107
|
operation_id=operation_id or None,
|
|
1102
1108
|
)
|
|
1103
|
-
|
|
1104
|
-
run_probes(loaded_schema, config)
|
|
1109
|
+
schema = load_schema(config)
|
|
1105
1110
|
yield from runner.from_schema(
|
|
1106
|
-
|
|
1111
|
+
schema,
|
|
1107
1112
|
auth=auth,
|
|
1108
1113
|
auth_type=auth_type,
|
|
1109
1114
|
override=override,
|
|
@@ -1126,6 +1131,16 @@ def into_event_stream(
|
|
|
1126
1131
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
1127
1132
|
hypothesis_settings=hypothesis_settings,
|
|
1128
1133
|
generation_config=generation_config,
|
|
1134
|
+
probe_config=probes.ProbeConfig(
|
|
1135
|
+
base_url=config.base_url,
|
|
1136
|
+
request_tls_verify=config.request_tls_verify,
|
|
1137
|
+
request_proxy=config.request_proxy,
|
|
1138
|
+
request_cert=config.request_cert,
|
|
1139
|
+
auth=config.auth,
|
|
1140
|
+
auth_type=config.auth_type,
|
|
1141
|
+
headers=config.headers,
|
|
1142
|
+
),
|
|
1143
|
+
service_client=service_client,
|
|
1129
1144
|
).execute()
|
|
1130
1145
|
except SchemaError as error:
|
|
1131
1146
|
yield events.InternalError.from_schema_error(error)
|
|
@@ -1133,19 +1148,6 @@ def into_event_stream(
|
|
|
1133
1148
|
yield events.InternalError.from_exc(exc)
|
|
1134
1149
|
|
|
1135
1150
|
|
|
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
1151
|
def load_schema(config: LoaderConfig) -> BaseSchema:
|
|
1150
1152
|
"""Automatically load API schema."""
|
|
1151
1153
|
first: Callable[[LoaderConfig], BaseSchema]
|
schemathesis/cli/cassettes.py
CHANGED
|
@@ -254,8 +254,8 @@ def write_double_quoted(stream: IO, text: str) -> None:
|
|
|
254
254
|
ch = text[end]
|
|
255
255
|
if (
|
|
256
256
|
ch is None
|
|
257
|
-
or ch in '"\\\x85\u2028\u2029\
|
|
258
|
-
or not ("\x20" <= ch <= "\
|
|
257
|
+
or ch in '"\\\x85\u2028\u2029\ufeff'
|
|
258
|
+
or not ("\x20" <= ch <= "\x7e" or ("\xa0" <= ch <= "\ud7ff" or "\ue000" <= ch <= "\ufffd"))
|
|
259
259
|
):
|
|
260
260
|
if start < end:
|
|
261
261
|
stream.write(text[start:end])
|
|
@@ -264,9 +264,9 @@ def write_double_quoted(stream: IO, text: str) -> None:
|
|
|
264
264
|
# Escape character
|
|
265
265
|
if ch in Emitter.ESCAPE_REPLACEMENTS:
|
|
266
266
|
data = "\\" + Emitter.ESCAPE_REPLACEMENTS[ch]
|
|
267
|
-
elif ch <= "\
|
|
267
|
+
elif ch <= "\xff":
|
|
268
268
|
data = "\\x%02X" % ord(ch)
|
|
269
|
-
elif ch <= "\
|
|
269
|
+
elif ch <= "\uffff":
|
|
270
270
|
data = "\\u%04X" % ord(ch)
|
|
271
271
|
else:
|
|
272
272
|
data = "\\U%08X" % ord(ch)
|
schemathesis/cli/context.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import os
|
|
3
4
|
import shutil
|
|
4
5
|
from dataclasses import dataclass, field
|
|
@@ -7,7 +8,10 @@ from typing import TYPE_CHECKING
|
|
|
7
8
|
|
|
8
9
|
from ..code_samples import CodeSampleStyle
|
|
9
10
|
from ..internal.deprecation import deprecated_property
|
|
11
|
+
from ..internal.result import Result
|
|
12
|
+
from ..runner.probes import ProbeRun
|
|
10
13
|
from ..runner.serialization import SerializedTestResult
|
|
14
|
+
from ..service.models import AnalysisResult
|
|
11
15
|
|
|
12
16
|
if TYPE_CHECKING:
|
|
13
17
|
import hypothesis
|
|
@@ -49,6 +53,8 @@ class ExecutionContext:
|
|
|
49
53
|
verbosity: int = 0
|
|
50
54
|
code_sample_style: CodeSampleStyle = CodeSampleStyle.default()
|
|
51
55
|
report: ServiceReportContext | FileReportContext | None = None
|
|
56
|
+
probes: list[ProbeRun] | None = None
|
|
57
|
+
analysis: Result[AnalysisResult, Exception] | None = None
|
|
52
58
|
|
|
53
59
|
@deprecated_property(removed_in="4.0", replacement="show_trace")
|
|
54
60
|
def show_errors_tracebacks(self) -> bool:
|
|
@@ -1,38 +1,50 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import base64
|
|
3
4
|
import os
|
|
4
5
|
import platform
|
|
5
6
|
import shutil
|
|
6
7
|
import textwrap
|
|
7
8
|
import time
|
|
9
|
+
from importlib import metadata
|
|
8
10
|
from itertools import groupby
|
|
9
11
|
from queue import Queue
|
|
10
|
-
from typing import Any, Generator, cast
|
|
12
|
+
from typing import Any, Generator, cast, TYPE_CHECKING
|
|
11
13
|
|
|
12
14
|
import click
|
|
13
|
-
from importlib import metadata
|
|
14
15
|
|
|
15
16
|
from ... import service
|
|
16
17
|
from ...code_samples import CodeSampleStyle
|
|
17
18
|
from ...constants import (
|
|
18
19
|
DISCORD_LINK,
|
|
20
|
+
FALSE_VALUES,
|
|
19
21
|
FLAKY_FAILURE_MESSAGE,
|
|
22
|
+
GITHUB_APP_LINK,
|
|
23
|
+
ISSUE_TRACKER_URL,
|
|
20
24
|
REPORT_SUGGESTION_ENV_VAR,
|
|
21
25
|
SCHEMATHESIS_TEST_CASE_HEADER,
|
|
22
26
|
SCHEMATHESIS_VERSION,
|
|
23
|
-
FALSE_VALUES,
|
|
24
|
-
ISSUE_TRACKER_URL,
|
|
25
|
-
GITHUB_APP_LINK,
|
|
26
27
|
)
|
|
27
|
-
from ...exceptions import
|
|
28
|
+
from ...exceptions import (
|
|
29
|
+
RuntimeErrorType,
|
|
30
|
+
format_exception,
|
|
31
|
+
prepare_response_payload,
|
|
32
|
+
extract_requests_exception_details,
|
|
33
|
+
)
|
|
28
34
|
from ...experimental import GLOBAL_EXPERIMENTS
|
|
35
|
+
from ...internal.result import Ok
|
|
29
36
|
from ...models import Status
|
|
30
37
|
from ...runner import events
|
|
31
38
|
from ...runner.events import InternalErrorType, SchemaErrorType
|
|
32
|
-
from ...runner.
|
|
39
|
+
from ...runner.probes import ProbeOutcome
|
|
40
|
+
from ...runner.serialization import SerializedCheck, SerializedError, SerializedTestResult, deduplicate_failures
|
|
41
|
+
from ...service.models import AnalysisSuccess, UnknownExtension, ErrorState
|
|
33
42
|
from ..context import ExecutionContext, FileReportContext, ServiceReportContext
|
|
34
43
|
from ..handlers import EventHandler
|
|
35
44
|
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
import requests
|
|
47
|
+
|
|
36
48
|
SPINNER_REPETITION_NUMBER = 10
|
|
37
49
|
|
|
38
50
|
|
|
@@ -179,16 +191,19 @@ def display_generic_errors(context: ExecutionContext, errors: list[SerializedErr
|
|
|
179
191
|
|
|
180
192
|
def display_full_traceback_message(error: SerializedError) -> bool:
|
|
181
193
|
# Some errors should not trigger the message that suggests to show full tracebacks to the user
|
|
182
|
-
return
|
|
183
|
-
(
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
194
|
+
return (
|
|
195
|
+
not error.exception.startswith(
|
|
196
|
+
(
|
|
197
|
+
"DeadlineExceeded",
|
|
198
|
+
"OperationSchemaError",
|
|
199
|
+
"requests.exceptions",
|
|
200
|
+
"SerializationNotPossible",
|
|
201
|
+
"hypothesis.errors.FailedHealthCheck",
|
|
202
|
+
"hypothesis.errors.InvalidArgument: Scalar ",
|
|
203
|
+
"hypothesis.errors.InvalidArgument: min_size=",
|
|
204
|
+
)
|
|
191
205
|
)
|
|
206
|
+
and "can never generate an example, because min_size is larger than Hypothesis supports." not in error.exception
|
|
192
207
|
)
|
|
193
208
|
|
|
194
209
|
|
|
@@ -357,6 +372,81 @@ def display_single_log(result: SerializedTestResult) -> None:
|
|
|
357
372
|
click.echo("\n\n".join(result.logs))
|
|
358
373
|
|
|
359
374
|
|
|
375
|
+
def display_analysis(context: ExecutionContext) -> None:
|
|
376
|
+
"""Display schema analysis details."""
|
|
377
|
+
import requests.exceptions
|
|
378
|
+
|
|
379
|
+
if context.analysis is None:
|
|
380
|
+
return
|
|
381
|
+
display_section_name("SCHEMA ANALYSIS")
|
|
382
|
+
if isinstance(context.analysis, Ok):
|
|
383
|
+
analysis = context.analysis.ok()
|
|
384
|
+
click.echo()
|
|
385
|
+
if isinstance(analysis, AnalysisSuccess):
|
|
386
|
+
click.secho(analysis.message, bold=True)
|
|
387
|
+
click.echo("\nAnalysis took: {:.2f}ms".format(analysis.elapsed))
|
|
388
|
+
if analysis.extensions:
|
|
389
|
+
known = []
|
|
390
|
+
failed = []
|
|
391
|
+
unknown = []
|
|
392
|
+
for extension in analysis.extensions:
|
|
393
|
+
if isinstance(extension, UnknownExtension):
|
|
394
|
+
unknown.append(extension)
|
|
395
|
+
elif isinstance(extension.state, ErrorState):
|
|
396
|
+
failed.append(extension)
|
|
397
|
+
else:
|
|
398
|
+
known.append(extension)
|
|
399
|
+
if known:
|
|
400
|
+
click.echo("\nThe following extensions have been applied:\n")
|
|
401
|
+
for extension in known:
|
|
402
|
+
click.echo(f" - {extension.summary}")
|
|
403
|
+
if failed:
|
|
404
|
+
click.echo("\nThe following extensions errored:\n")
|
|
405
|
+
for extension in failed:
|
|
406
|
+
click.echo(f" - {extension.summary}")
|
|
407
|
+
suggestion = f"Please, consider reporting this to our issue tracker:\n\n {ISSUE_TRACKER_URL}"
|
|
408
|
+
click.secho(f"\n{click.style('Tip:', bold=True, fg='green')} {suggestion}")
|
|
409
|
+
if unknown:
|
|
410
|
+
noun = "extension" if len(unknown) == 1 else "extensions"
|
|
411
|
+
specific_noun = "this extension" if len(unknown) == 1 else "these extensions"
|
|
412
|
+
title = click.style("Compatibility Notice", bold=True)
|
|
413
|
+
click.secho(f"\n{title}: {len(unknown)} {noun} not recognized:\n")
|
|
414
|
+
for extension in unknown:
|
|
415
|
+
click.echo(f" - {extension.summary}")
|
|
416
|
+
suggestion = f"Consider updating the CLI to add support for {specific_noun}."
|
|
417
|
+
click.secho(f"\n{click.style('Tip:', bold=True, fg='green')} {suggestion}")
|
|
418
|
+
else:
|
|
419
|
+
click.echo("\nNo extensions have been applied.")
|
|
420
|
+
else:
|
|
421
|
+
click.echo("An error happened during schema analysis:\n")
|
|
422
|
+
click.secho(f" {analysis.message}", bold=True)
|
|
423
|
+
click.echo()
|
|
424
|
+
else:
|
|
425
|
+
exception = context.analysis.err()
|
|
426
|
+
suggestion = None
|
|
427
|
+
if isinstance(exception, requests.exceptions.HTTPError):
|
|
428
|
+
response = exception.response
|
|
429
|
+
click.secho("Error\n", fg="red", bold=True)
|
|
430
|
+
_display_service_network_error(response)
|
|
431
|
+
click.echo()
|
|
432
|
+
return None
|
|
433
|
+
elif isinstance(exception, requests.RequestException):
|
|
434
|
+
message, extras = extract_requests_exception_details(exception)
|
|
435
|
+
suggestion = "Please check your network connection and try again."
|
|
436
|
+
title = "Network Error"
|
|
437
|
+
else:
|
|
438
|
+
traceback = format_exception(exception, True)
|
|
439
|
+
extras = _split_traceback(traceback)
|
|
440
|
+
title = "Internal Error"
|
|
441
|
+
message = f"We apologize for the inconvenience. This appears to be an internal issue.\nPlease, consider reporting the following details to our issue tracker:\n\n {ISSUE_TRACKER_URL}"
|
|
442
|
+
suggestion = "Please update your CLI to the latest version and try again."
|
|
443
|
+
click.secho(f"{title}\n", fg="red", bold=True)
|
|
444
|
+
click.echo(message)
|
|
445
|
+
_display_extras(extras)
|
|
446
|
+
_maybe_display_tip(suggestion)
|
|
447
|
+
click.echo()
|
|
448
|
+
|
|
449
|
+
|
|
360
450
|
def display_statistic(context: ExecutionContext, event: events.Finished) -> None:
|
|
361
451
|
"""Format and print statistic collected by :obj:`models.TestResult`."""
|
|
362
452
|
display_section_name("SUMMARY")
|
|
@@ -487,43 +577,49 @@ def display_service_unauthorized(hostname: str) -> None:
|
|
|
487
577
|
|
|
488
578
|
def display_service_error(event: service.Error, message_prefix: str = "") -> None:
|
|
489
579
|
"""Show information about an error during communication with Schemathesis.io."""
|
|
490
|
-
from requests import
|
|
580
|
+
from requests import HTTPError, RequestException, Response
|
|
491
581
|
|
|
492
582
|
if isinstance(event.exception, HTTPError):
|
|
493
583
|
response = cast(Response, event.exception.response)
|
|
494
|
-
|
|
495
|
-
if 500 <= status_code <= 599:
|
|
496
|
-
click.secho(f"Schemathesis.io responded with HTTP {status_code}", fg="red")
|
|
497
|
-
# Server error, should be resolved soon
|
|
498
|
-
click.secho(
|
|
499
|
-
"\nIt is likely that we are already notified about the issue and working on a fix\n"
|
|
500
|
-
"Please, try again in 30 minutes",
|
|
501
|
-
fg="red",
|
|
502
|
-
)
|
|
503
|
-
elif status_code == 401:
|
|
504
|
-
# Likely an invalid token
|
|
505
|
-
click.echo("Your CLI is not authenticated.")
|
|
506
|
-
display_service_unauthorized("schemathesis.io")
|
|
507
|
-
else:
|
|
508
|
-
try:
|
|
509
|
-
data = response.json()
|
|
510
|
-
detail = data["detail"]
|
|
511
|
-
click.secho(f"{message_prefix}{detail}", fg="red")
|
|
512
|
-
except Exception:
|
|
513
|
-
# Other client-side errors are likely caused by a bug on the CLI side
|
|
514
|
-
click.secho(
|
|
515
|
-
"We apologize for the inconvenience. This appears to be an internal issue.\n"
|
|
516
|
-
"Please, consider reporting the following details to our issue "
|
|
517
|
-
f"tracker:\n\n {ISSUE_TRACKER_URL}\n\nResponse: {response.text!r}\n"
|
|
518
|
-
f"Headers: {response.headers!r}",
|
|
519
|
-
fg="red",
|
|
520
|
-
)
|
|
584
|
+
_display_service_network_error(response, message_prefix)
|
|
521
585
|
elif isinstance(event.exception, RequestException):
|
|
522
586
|
ask_to_report(event, report_to_issues=False)
|
|
523
587
|
else:
|
|
524
588
|
ask_to_report(event)
|
|
525
589
|
|
|
526
590
|
|
|
591
|
+
def _display_service_network_error(response: requests.Response, message_prefix: str = "") -> None:
|
|
592
|
+
status_code = response.status_code
|
|
593
|
+
if 500 <= status_code <= 599:
|
|
594
|
+
click.secho(f"Schemathesis.io responded with HTTP {status_code}", fg="red")
|
|
595
|
+
# Server error, should be resolved soon
|
|
596
|
+
click.secho(
|
|
597
|
+
"\nIt is likely that we are already notified about the issue and working on a fix\n"
|
|
598
|
+
"Please, try again in 30 minutes",
|
|
599
|
+
fg="red",
|
|
600
|
+
)
|
|
601
|
+
elif status_code == 401:
|
|
602
|
+
# Likely an invalid token
|
|
603
|
+
click.echo("Your CLI is not authenticated.")
|
|
604
|
+
display_service_unauthorized("schemathesis.io")
|
|
605
|
+
else:
|
|
606
|
+
try:
|
|
607
|
+
data = response.json()
|
|
608
|
+
detail = data["detail"]
|
|
609
|
+
click.secho(f"{message_prefix}{detail}", fg="red")
|
|
610
|
+
except Exception:
|
|
611
|
+
# Other client-side errors are likely caused by a bug on the CLI side
|
|
612
|
+
click.secho(
|
|
613
|
+
"We apologize for the inconvenience. This appears to be an internal issue.\n"
|
|
614
|
+
"Please, consider reporting the following details to our issue "
|
|
615
|
+
f"tracker:\n\n {ISSUE_TRACKER_URL}\n\nResponse: {response.text!r}\n"
|
|
616
|
+
f"Status: {response.status_code}\n"
|
|
617
|
+
f"Headers: {response.headers!r}",
|
|
618
|
+
fg="red",
|
|
619
|
+
)
|
|
620
|
+
_maybe_display_tip("Please update your CLI to the latest version and try again.")
|
|
621
|
+
|
|
622
|
+
|
|
527
623
|
SERVICE_ERROR_MESSAGE = "An error happened during uploading reports to Schemathesis.io"
|
|
528
624
|
|
|
529
625
|
|
|
@@ -694,7 +790,42 @@ def handle_initialized(context: ExecutionContext, event: events.Initialized) ->
|
|
|
694
790
|
click.secho(f"Collected API links: {links_count}", bold=True)
|
|
695
791
|
if isinstance(context.report, ServiceReportContext):
|
|
696
792
|
click.secho("Report to Schemathesis.io: ENABLED", bold=True)
|
|
697
|
-
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def handle_before_probing(context: ExecutionContext, event: events.BeforeProbing) -> None:
|
|
796
|
+
click.secho("API probing: ...\r", bold=True, nl=False)
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def handle_after_probing(context: ExecutionContext, event: events.AfterProbing) -> None:
|
|
800
|
+
context.probes = event.probes
|
|
801
|
+
status = "SKIP"
|
|
802
|
+
if event.probes is not None:
|
|
803
|
+
for probe in event.probes:
|
|
804
|
+
if probe.outcome in (ProbeOutcome.SUCCESS, ProbeOutcome.FAILURE):
|
|
805
|
+
# The probe itself has been executed
|
|
806
|
+
status = "SUCCESS"
|
|
807
|
+
elif probe.outcome == ProbeOutcome.ERROR:
|
|
808
|
+
status = "ERROR"
|
|
809
|
+
click.secho(f"API probing: {status}", bold=True, nl=False)
|
|
810
|
+
click.echo()
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def handle_before_analysis(context: ExecutionContext, event: events.BeforeAnalysis) -> None:
|
|
814
|
+
click.secho("Schema analysis: ...\r", bold=True, nl=False)
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def handle_after_analysis(context: ExecutionContext, event: events.AfterAnalysis) -> None:
|
|
818
|
+
context.analysis = event.analysis
|
|
819
|
+
status = "SKIP"
|
|
820
|
+
if event.analysis is not None:
|
|
821
|
+
if isinstance(event.analysis, Ok) and isinstance(event.analysis.ok(), AnalysisSuccess):
|
|
822
|
+
status = "SUCCESS"
|
|
823
|
+
else:
|
|
824
|
+
status = "ERROR"
|
|
825
|
+
click.secho(f"Schema analysis: {status}", bold=True, nl=False)
|
|
826
|
+
click.echo()
|
|
827
|
+
operations_count = cast(int, context.operations_count) # INVARIANT: should not be `None`
|
|
828
|
+
if operations_count >= 1:
|
|
698
829
|
click.echo()
|
|
699
830
|
|
|
700
831
|
|
|
@@ -731,6 +862,7 @@ def handle_finished(context: ExecutionContext, event: events.Finished) -> None:
|
|
|
731
862
|
display_errors(context, event)
|
|
732
863
|
display_failures(context, event)
|
|
733
864
|
display_application_logs(context, event)
|
|
865
|
+
display_analysis(context)
|
|
734
866
|
display_statistic(context, event)
|
|
735
867
|
click.echo()
|
|
736
868
|
display_summary(event)
|
|
@@ -752,6 +884,14 @@ class DefaultOutputStyleHandler(EventHandler):
|
|
|
752
884
|
"""Choose and execute a proper handler for the given event."""
|
|
753
885
|
if isinstance(event, events.Initialized):
|
|
754
886
|
handle_initialized(context, event)
|
|
887
|
+
if isinstance(event, events.BeforeProbing):
|
|
888
|
+
handle_before_probing(context, event)
|
|
889
|
+
if isinstance(event, events.AfterProbing):
|
|
890
|
+
handle_after_probing(context, event)
|
|
891
|
+
if isinstance(event, events.BeforeAnalysis):
|
|
892
|
+
handle_before_analysis(context, event)
|
|
893
|
+
if isinstance(event, events.AfterAnalysis):
|
|
894
|
+
handle_after_analysis(context, event)
|
|
755
895
|
if isinstance(event, events.BeforeExecution):
|
|
756
896
|
handle_before_execution(context, event)
|
|
757
897
|
if isinstance(event, events.AfterExecution):
|
schemathesis/cli/output/short.py
CHANGED
|
@@ -26,6 +26,14 @@ class ShortOutputStyleHandler(EventHandler):
|
|
|
26
26
|
"""
|
|
27
27
|
if isinstance(event, events.Initialized):
|
|
28
28
|
default.handle_initialized(context, event)
|
|
29
|
+
if isinstance(event, events.BeforeProbing):
|
|
30
|
+
default.handle_before_probing(context, event)
|
|
31
|
+
if isinstance(event, events.AfterProbing):
|
|
32
|
+
default.handle_after_probing(context, event)
|
|
33
|
+
if isinstance(event, events.BeforeAnalysis):
|
|
34
|
+
default.handle_before_analysis(context, event)
|
|
35
|
+
if isinstance(event, events.AfterAnalysis):
|
|
36
|
+
default.handle_after_analysis(context, event)
|
|
29
37
|
if isinstance(event, events.BeforeExecution):
|
|
30
38
|
handle_before_execution(context, event)
|
|
31
39
|
if isinstance(event, events.AfterExecution):
|
|
@@ -72,3 +72,10 @@ OPEN_API_3_1 = GLOBAL_EXPERIMENTS.create_experiment(
|
|
|
72
72
|
description="Support for response validation",
|
|
73
73
|
discussion_url="https://github.com/schemathesis/schemathesis/discussions/1822",
|
|
74
74
|
)
|
|
75
|
+
SCHEMA_ANALYSIS = GLOBAL_EXPERIMENTS.create_experiment(
|
|
76
|
+
name="schema-analysis",
|
|
77
|
+
verbose_name="Schema Analysis",
|
|
78
|
+
env_var="SCHEMA_ANALYSIS",
|
|
79
|
+
description="Analyzing API schemas via Schemathesis.io",
|
|
80
|
+
discussion_url="https://github.com/schemathesis/schemathesis/discussions/2056",
|
|
81
|
+
)
|
schemathesis/filters.py
CHANGED
schemathesis/models.py
CHANGED
|
@@ -960,9 +960,12 @@ class Response:
|
|
|
960
960
|
@classmethod
|
|
961
961
|
def from_requests(cls, response: requests.Response) -> Response:
|
|
962
962
|
"""Create a response from requests.Response."""
|
|
963
|
-
|
|
963
|
+
raw = response.raw
|
|
964
|
+
raw_headers = raw.headers if raw is not None else {}
|
|
965
|
+
headers = {name: response.raw.headers.getlist(name) for name in raw_headers.keys()}
|
|
964
966
|
# Similar to http.client:319 (HTTP version detection in stdlib's `http` package)
|
|
965
|
-
|
|
967
|
+
version = raw.version if raw is not None else 10
|
|
968
|
+
http_version = "1.0" if version == 10 else "1.1"
|
|
966
969
|
|
|
967
970
|
def is_empty(_response: requests.Response) -> bool:
|
|
968
971
|
# Assume the response is empty if:
|
schemathesis/parameters.py
CHANGED