schemathesis 3.25.6__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 +26 -18
- schemathesis/cli/cassettes.py +4 -4
- schemathesis/cli/context.py +4 -1
- schemathesis/cli/output/default.py +154 -39
- schemathesis/cli/output/short.py +4 -0
- schemathesis/experimental/__init__.py +7 -0
- schemathesis/filters.py +1 -0
- schemathesis/parameters.py +1 -0
- schemathesis/runner/__init__.py +12 -0
- schemathesis/runner/events.py +12 -0
- schemathesis/runner/impl/core.py +29 -2
- schemathesis/runner/probes.py +1 -0
- schemathesis/runner/serialization.py +4 -2
- schemathesis/schemas.py +1 -0
- schemathesis/serializers.py +1 -1
- 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 +29 -1
- schemathesis/specs/openapi/__init__.py +1 -0
- schemathesis/specs/openapi/_hypothesis.py +8 -0
- 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/security.py +5 -1
- {schemathesis-3.25.6.dist-info → schemathesis-3.26.0.dist-info}/METADATA +8 -5
- {schemathesis-3.25.6.dist-info → schemathesis-3.26.0.dist-info}/RECORD +39 -37
- {schemathesis-3.25.6.dist-info → schemathesis-3.26.0.dist-info}/WHEEL +0 -0
- {schemathesis-3.25.6.dist-info → schemathesis-3.26.0.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.25.6.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,47 +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
44
|
from ..runner import events, prepare_hypothesis_settings, probes
|
|
45
45
|
from ..specs.graphql import loaders as gql_loaders
|
|
46
46
|
from ..specs.openapi import loaders as oas_loaders
|
|
47
47
|
from ..stateful import Stateful
|
|
48
48
|
from ..targets import Target
|
|
49
|
+
from ..transports.auth import get_requests_auth
|
|
49
50
|
from ..types import Filter, PathLike, RequestCert
|
|
50
|
-
from ..internal.datetime import current_datetime
|
|
51
|
-
from ..internal.validation import file_exists
|
|
52
51
|
from . import callbacks, cassettes, output
|
|
53
|
-
from .constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS
|
|
52
|
+
from .constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS, HealthCheck, Phase, Verbosity
|
|
54
53
|
from .context import ExecutionContext, FileReportContext, ServiceReportContext
|
|
55
54
|
from .debug import DebugOutputHandler
|
|
56
55
|
from .junitxml import JunitXMLHandler
|
|
@@ -60,8 +59,9 @@ from .sanitization import SanitizationHandler
|
|
|
60
59
|
if TYPE_CHECKING:
|
|
61
60
|
import hypothesis
|
|
62
61
|
import requests
|
|
63
|
-
|
|
62
|
+
|
|
64
63
|
from ..schemas import BaseSchema
|
|
64
|
+
from ..service.client import ServiceClient
|
|
65
65
|
from ..specs.graphql.schemas import GraphQLSchema
|
|
66
66
|
from .handlers import EventHandler
|
|
67
67
|
|
|
@@ -681,8 +681,9 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
|
|
|
681
681
|
@click.option("--force-color", help="Explicitly tells to enable ANSI color escape codes.", type=bool, is_flag=True)
|
|
682
682
|
@click.option(
|
|
683
683
|
"--experimental",
|
|
684
|
+
"experiments",
|
|
684
685
|
help="Enable experimental support for specific features.",
|
|
685
|
-
type=click.Choice([experimental.OPEN_API_3_1.name]),
|
|
686
|
+
type=click.Choice([experimental.OPEN_API_3_1.name, experimental.SCHEMA_ANALYSIS.name]),
|
|
686
687
|
callback=callbacks.convert_experimental,
|
|
687
688
|
multiple=True,
|
|
688
689
|
)
|
|
@@ -737,7 +738,7 @@ def run(
|
|
|
737
738
|
set_header: dict[str, str],
|
|
738
739
|
set_cookie: dict[str, str],
|
|
739
740
|
set_path: dict[str, str],
|
|
740
|
-
|
|
741
|
+
experiments: list,
|
|
741
742
|
checks: Iterable[str] = DEFAULT_CHECKS_NAMES,
|
|
742
743
|
exclude_checks: Iterable[str] = (),
|
|
743
744
|
data_generation_methods: tuple[DataGenerationMethod, ...] = DEFAULT_DATA_GENERATION_METHODS,
|
|
@@ -826,7 +827,7 @@ def run(
|
|
|
826
827
|
show_trace = show_errors_tracebacks
|
|
827
828
|
|
|
828
829
|
# Enable selected experiments
|
|
829
|
-
for experiment in
|
|
830
|
+
for experiment in experiments:
|
|
830
831
|
experiment.enable()
|
|
831
832
|
|
|
832
833
|
override = CaseOverride(query=set_query, headers=set_header, cookies=set_cookie, path_parameters=set_path)
|
|
@@ -901,6 +902,10 @@ def run(
|
|
|
901
902
|
from ..service.client import ServiceClient
|
|
902
903
|
|
|
903
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
|
+
|
|
904
909
|
client = ServiceClient(base_url=schemathesis_io_url, token=token)
|
|
905
910
|
host_data = service.hosts.HostData(schemathesis_io_hostname, hosts_file)
|
|
906
911
|
|
|
@@ -970,6 +975,7 @@ def run(
|
|
|
970
975
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
971
976
|
hypothesis_settings=hypothesis_settings,
|
|
972
977
|
generation_config=generation_config,
|
|
978
|
+
service_client=client,
|
|
973
979
|
)
|
|
974
980
|
execute(
|
|
975
981
|
event_stream,
|
|
@@ -1074,6 +1080,7 @@ def into_event_stream(
|
|
|
1074
1080
|
store_interactions: bool,
|
|
1075
1081
|
stateful: Stateful | None,
|
|
1076
1082
|
stateful_recursion_limit: int,
|
|
1083
|
+
service_client: ServiceClient | None,
|
|
1077
1084
|
) -> Generator[events.ExecutionEvent, None, None]:
|
|
1078
1085
|
try:
|
|
1079
1086
|
if app is not None:
|
|
@@ -1133,6 +1140,7 @@ def into_event_stream(
|
|
|
1133
1140
|
auth_type=config.auth_type,
|
|
1134
1141
|
headers=config.headers,
|
|
1135
1142
|
),
|
|
1143
|
+
service_client=service_client,
|
|
1136
1144
|
).execute()
|
|
1137
1145
|
except SchemaError as error:
|
|
1138
1146
|
yield events.InternalError.from_schema_error(error)
|
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
|
@@ -8,8 +8,10 @@ from typing import TYPE_CHECKING
|
|
|
8
8
|
|
|
9
9
|
from ..code_samples import CodeSampleStyle
|
|
10
10
|
from ..internal.deprecation import deprecated_property
|
|
11
|
-
from ..
|
|
11
|
+
from ..internal.result import Result
|
|
12
12
|
from ..runner.probes import ProbeRun
|
|
13
|
+
from ..runner.serialization import SerializedTestResult
|
|
14
|
+
from ..service.models import AnalysisResult
|
|
13
15
|
|
|
14
16
|
if TYPE_CHECKING:
|
|
15
17
|
import hypothesis
|
|
@@ -52,6 +54,7 @@ class ExecutionContext:
|
|
|
52
54
|
code_sample_style: CodeSampleStyle = CodeSampleStyle.default()
|
|
53
55
|
report: ServiceReportContext | FileReportContext | None = None
|
|
54
56
|
probes: list[ProbeRun] | None = None
|
|
57
|
+
analysis: Result[AnalysisResult, Exception] | None = None
|
|
55
58
|
|
|
56
59
|
@deprecated_property(removed_in="4.0", replacement="show_trace")
|
|
57
60
|
def show_errors_tracebacks(self) -> bool:
|
|
@@ -9,7 +9,7 @@ import time
|
|
|
9
9
|
from importlib import metadata
|
|
10
10
|
from itertools import groupby
|
|
11
11
|
from queue import Queue
|
|
12
|
-
from typing import Any, Generator, cast
|
|
12
|
+
from typing import Any, Generator, cast, TYPE_CHECKING
|
|
13
13
|
|
|
14
14
|
import click
|
|
15
15
|
|
|
@@ -25,16 +25,26 @@ from ...constants import (
|
|
|
25
25
|
SCHEMATHESIS_TEST_CASE_HEADER,
|
|
26
26
|
SCHEMATHESIS_VERSION,
|
|
27
27
|
)
|
|
28
|
-
from ...exceptions import
|
|
28
|
+
from ...exceptions import (
|
|
29
|
+
RuntimeErrorType,
|
|
30
|
+
format_exception,
|
|
31
|
+
prepare_response_payload,
|
|
32
|
+
extract_requests_exception_details,
|
|
33
|
+
)
|
|
29
34
|
from ...experimental import GLOBAL_EXPERIMENTS
|
|
35
|
+
from ...internal.result import Ok
|
|
30
36
|
from ...models import Status
|
|
31
37
|
from ...runner import events
|
|
32
38
|
from ...runner.events import InternalErrorType, SchemaErrorType
|
|
33
39
|
from ...runner.probes import ProbeOutcome
|
|
34
40
|
from ...runner.serialization import SerializedCheck, SerializedError, SerializedTestResult, deduplicate_failures
|
|
41
|
+
from ...service.models import AnalysisSuccess, UnknownExtension, ErrorState
|
|
35
42
|
from ..context import ExecutionContext, FileReportContext, ServiceReportContext
|
|
36
43
|
from ..handlers import EventHandler
|
|
37
44
|
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
import requests
|
|
47
|
+
|
|
38
48
|
SPINNER_REPETITION_NUMBER = 10
|
|
39
49
|
|
|
40
50
|
|
|
@@ -181,16 +191,19 @@ def display_generic_errors(context: ExecutionContext, errors: list[SerializedErr
|
|
|
181
191
|
|
|
182
192
|
def display_full_traceback_message(error: SerializedError) -> bool:
|
|
183
193
|
# Some errors should not trigger the message that suggests to show full tracebacks to the user
|
|
184
|
-
return
|
|
185
|
-
(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
+
)
|
|
193
205
|
)
|
|
206
|
+
and "can never generate an example, because min_size is larger than Hypothesis supports." not in error.exception
|
|
194
207
|
)
|
|
195
208
|
|
|
196
209
|
|
|
@@ -359,6 +372,81 @@ def display_single_log(result: SerializedTestResult) -> None:
|
|
|
359
372
|
click.echo("\n\n".join(result.logs))
|
|
360
373
|
|
|
361
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
|
+
|
|
362
450
|
def display_statistic(context: ExecutionContext, event: events.Finished) -> None:
|
|
363
451
|
"""Format and print statistic collected by :obj:`models.TestResult`."""
|
|
364
452
|
display_section_name("SUMMARY")
|
|
@@ -493,39 +581,45 @@ def display_service_error(event: service.Error, message_prefix: str = "") -> Non
|
|
|
493
581
|
|
|
494
582
|
if isinstance(event.exception, HTTPError):
|
|
495
583
|
response = cast(Response, event.exception.response)
|
|
496
|
-
|
|
497
|
-
if 500 <= status_code <= 599:
|
|
498
|
-
click.secho(f"Schemathesis.io responded with HTTP {status_code}", fg="red")
|
|
499
|
-
# Server error, should be resolved soon
|
|
500
|
-
click.secho(
|
|
501
|
-
"\nIt is likely that we are already notified about the issue and working on a fix\n"
|
|
502
|
-
"Please, try again in 30 minutes",
|
|
503
|
-
fg="red",
|
|
504
|
-
)
|
|
505
|
-
elif status_code == 401:
|
|
506
|
-
# Likely an invalid token
|
|
507
|
-
click.echo("Your CLI is not authenticated.")
|
|
508
|
-
display_service_unauthorized("schemathesis.io")
|
|
509
|
-
else:
|
|
510
|
-
try:
|
|
511
|
-
data = response.json()
|
|
512
|
-
detail = data["detail"]
|
|
513
|
-
click.secho(f"{message_prefix}{detail}", fg="red")
|
|
514
|
-
except Exception:
|
|
515
|
-
# Other client-side errors are likely caused by a bug on the CLI side
|
|
516
|
-
click.secho(
|
|
517
|
-
"We apologize for the inconvenience. This appears to be an internal issue.\n"
|
|
518
|
-
"Please, consider reporting the following details to our issue "
|
|
519
|
-
f"tracker:\n\n {ISSUE_TRACKER_URL}\n\nResponse: {response.text!r}\n"
|
|
520
|
-
f"Headers: {response.headers!r}",
|
|
521
|
-
fg="red",
|
|
522
|
-
)
|
|
584
|
+
_display_service_network_error(response, message_prefix)
|
|
523
585
|
elif isinstance(event.exception, RequestException):
|
|
524
586
|
ask_to_report(event, report_to_issues=False)
|
|
525
587
|
else:
|
|
526
588
|
ask_to_report(event)
|
|
527
589
|
|
|
528
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
|
+
|
|
529
623
|
SERVICE_ERROR_MESSAGE = "An error happened during uploading reports to Schemathesis.io"
|
|
530
624
|
|
|
531
625
|
|
|
@@ -712,7 +806,23 @@ def handle_after_probing(context: ExecutionContext, event: events.AfterProbing)
|
|
|
712
806
|
status = "SUCCESS"
|
|
713
807
|
elif probe.outcome == ProbeOutcome.ERROR:
|
|
714
808
|
status = "ERROR"
|
|
715
|
-
click.secho(f"API probing: {status}
|
|
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)
|
|
716
826
|
click.echo()
|
|
717
827
|
operations_count = cast(int, context.operations_count) # INVARIANT: should not be `None`
|
|
718
828
|
if operations_count >= 1:
|
|
@@ -752,6 +862,7 @@ def handle_finished(context: ExecutionContext, event: events.Finished) -> None:
|
|
|
752
862
|
display_errors(context, event)
|
|
753
863
|
display_failures(context, event)
|
|
754
864
|
display_application_logs(context, event)
|
|
865
|
+
display_analysis(context)
|
|
755
866
|
display_statistic(context, event)
|
|
756
867
|
click.echo()
|
|
757
868
|
display_summary(event)
|
|
@@ -777,6 +888,10 @@ class DefaultOutputStyleHandler(EventHandler):
|
|
|
777
888
|
handle_before_probing(context, event)
|
|
778
889
|
if isinstance(event, events.AfterProbing):
|
|
779
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)
|
|
780
895
|
if isinstance(event, events.BeforeExecution):
|
|
781
896
|
handle_before_execution(context, event)
|
|
782
897
|
if isinstance(event, events.AfterExecution):
|
schemathesis/cli/output/short.py
CHANGED
|
@@ -30,6 +30,10 @@ class ShortOutputStyleHandler(EventHandler):
|
|
|
30
30
|
default.handle_before_probing(context, event)
|
|
31
31
|
if isinstance(event, events.AfterProbing):
|
|
32
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)
|
|
33
37
|
if isinstance(event, events.BeforeExecution):
|
|
34
38
|
handle_before_execution(context, event)
|
|
35
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/parameters.py
CHANGED
schemathesis/runner/__init__.py
CHANGED
|
@@ -31,6 +31,7 @@ if TYPE_CHECKING:
|
|
|
31
31
|
from ..stateful import Stateful
|
|
32
32
|
from . import events
|
|
33
33
|
from .impl import BaseRunner
|
|
34
|
+
from ..service.client import ServiceClient
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
@deprecated_function(removed_in="4.0", replacement="schemathesis.runner.from_schema")
|
|
@@ -78,6 +79,7 @@ def prepare(
|
|
|
78
79
|
hypothesis_suppress_health_check: list[hypothesis.HealthCheck] | None = None,
|
|
79
80
|
hypothesis_verbosity: hypothesis.Verbosity | None = None,
|
|
80
81
|
probe_config: ProbeConfig | None = None,
|
|
82
|
+
service_client: ServiceClient | None = None,
|
|
81
83
|
) -> Generator[events.ExecutionEvent, None, None]:
|
|
82
84
|
"""Prepare a generator that will run test cases against the given API definition."""
|
|
83
85
|
from ..checks import DEFAULT_CHECKS
|
|
@@ -132,6 +134,7 @@ def prepare(
|
|
|
132
134
|
count_operations=count_operations,
|
|
133
135
|
count_links=count_links,
|
|
134
136
|
probe_config=probe_config,
|
|
137
|
+
service_client=service_client,
|
|
135
138
|
)
|
|
136
139
|
|
|
137
140
|
|
|
@@ -193,6 +196,7 @@ def execute_from_schema(
|
|
|
193
196
|
count_operations: bool = True,
|
|
194
197
|
count_links: bool = True,
|
|
195
198
|
probe_config: ProbeConfig | None = None,
|
|
199
|
+
service_client: ServiceClient | None,
|
|
196
200
|
) -> Generator[events.ExecutionEvent, None, None]:
|
|
197
201
|
"""Execute tests for the given schema.
|
|
198
202
|
|
|
@@ -243,6 +247,7 @@ def execute_from_schema(
|
|
|
243
247
|
count_operations=count_operations,
|
|
244
248
|
count_links=count_links,
|
|
245
249
|
probe_config=probe_config,
|
|
250
|
+
service_client=service_client,
|
|
246
251
|
).execute()
|
|
247
252
|
except SchemaError as error:
|
|
248
253
|
yield events.InternalError.from_schema_error(error)
|
|
@@ -348,6 +353,7 @@ def from_schema(
|
|
|
348
353
|
count_operations: bool = True,
|
|
349
354
|
count_links: bool = True,
|
|
350
355
|
probe_config: ProbeConfig | None = None,
|
|
356
|
+
service_client: ServiceClient | None = None,
|
|
351
357
|
) -> BaseRunner:
|
|
352
358
|
import hypothesis
|
|
353
359
|
from starlette.applications import Starlette
|
|
@@ -402,6 +408,7 @@ def from_schema(
|
|
|
402
408
|
count_operations=count_operations,
|
|
403
409
|
count_links=count_links,
|
|
404
410
|
probe_config=probe_config,
|
|
411
|
+
service_client=service_client,
|
|
405
412
|
)
|
|
406
413
|
if isinstance(schema.app, Starlette):
|
|
407
414
|
return ThreadPoolASGIRunner(
|
|
@@ -426,6 +433,7 @@ def from_schema(
|
|
|
426
433
|
count_operations=count_operations,
|
|
427
434
|
count_links=count_links,
|
|
428
435
|
probe_config=probe_config,
|
|
436
|
+
service_client=service_client,
|
|
429
437
|
)
|
|
430
438
|
return ThreadPoolWSGIRunner(
|
|
431
439
|
schema=schema,
|
|
@@ -450,6 +458,7 @@ def from_schema(
|
|
|
450
458
|
count_operations=count_operations,
|
|
451
459
|
count_links=count_links,
|
|
452
460
|
probe_config=probe_config,
|
|
461
|
+
service_client=service_client,
|
|
453
462
|
)
|
|
454
463
|
if not schema.app:
|
|
455
464
|
return SingleThreadRunner(
|
|
@@ -478,6 +487,7 @@ def from_schema(
|
|
|
478
487
|
count_operations=count_operations,
|
|
479
488
|
count_links=count_links,
|
|
480
489
|
probe_config=probe_config,
|
|
490
|
+
service_client=service_client,
|
|
481
491
|
)
|
|
482
492
|
if isinstance(schema.app, Starlette):
|
|
483
493
|
return SingleThreadASGIRunner(
|
|
@@ -502,6 +512,7 @@ def from_schema(
|
|
|
502
512
|
count_operations=count_operations,
|
|
503
513
|
count_links=count_links,
|
|
504
514
|
probe_config=probe_config,
|
|
515
|
+
service_client=service_client,
|
|
505
516
|
)
|
|
506
517
|
return SingleThreadWSGIRunner(
|
|
507
518
|
schema=schema,
|
|
@@ -525,6 +536,7 @@ def from_schema(
|
|
|
525
536
|
count_operations=count_operations,
|
|
526
537
|
count_links=count_links,
|
|
527
538
|
probe_config=probe_config,
|
|
539
|
+
service_client=service_client,
|
|
528
540
|
)
|
|
529
541
|
|
|
530
542
|
|
schemathesis/runner/events.py
CHANGED
|
@@ -6,6 +6,7 @@ from dataclasses import asdict, dataclass, field
|
|
|
6
6
|
from typing import Any, TYPE_CHECKING
|
|
7
7
|
|
|
8
8
|
from ..internal.datetime import current_datetime
|
|
9
|
+
from ..internal.result import Result
|
|
9
10
|
from ..generation import DataGenerationMethod
|
|
10
11
|
from ..exceptions import SchemaError, SchemaErrorType, format_exception, RuntimeErrorType
|
|
11
12
|
from .serialization import SerializedError, SerializedTestResult
|
|
@@ -14,6 +15,7 @@ from .serialization import SerializedError, SerializedTestResult
|
|
|
14
15
|
if TYPE_CHECKING:
|
|
15
16
|
from ..models import APIOperation, Status, TestResult, TestResultSet
|
|
16
17
|
from ..schemas import BaseSchema
|
|
18
|
+
from ..service.models import AnalysisResult
|
|
17
19
|
from . import probes
|
|
18
20
|
|
|
19
21
|
|
|
@@ -93,6 +95,16 @@ class AfterProbing(ExecutionEvent):
|
|
|
93
95
|
return {"probes": [probe.serialize() for probe in probes], "events_type": self.__class__.__name__}
|
|
94
96
|
|
|
95
97
|
|
|
98
|
+
@dataclass
|
|
99
|
+
class BeforeAnalysis(ExecutionEvent):
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class AfterAnalysis(ExecutionEvent):
|
|
105
|
+
analysis: Result[AnalysisResult, Exception] | None
|
|
106
|
+
|
|
107
|
+
|
|
96
108
|
class CurrentOperationMixin:
|
|
97
109
|
method: str
|
|
98
110
|
path: str
|