schemathesis 3.25.6__py3-none-any.whl → 3.39.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +6 -6
- schemathesis/_compat.py +2 -2
- schemathesis/_dependency_versions.py +4 -2
- schemathesis/_hypothesis.py +369 -56
- schemathesis/_lazy_import.py +1 -0
- schemathesis/_override.py +5 -4
- schemathesis/_patches.py +21 -0
- schemathesis/_rate_limiter.py +7 -0
- schemathesis/_xml.py +75 -22
- schemathesis/auths.py +78 -16
- schemathesis/checks.py +21 -9
- schemathesis/cli/__init__.py +783 -432
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/callbacks.py +58 -13
- schemathesis/cli/cassettes.py +233 -47
- schemathesis/cli/constants.py +8 -2
- schemathesis/cli/context.py +22 -5
- schemathesis/cli/debug.py +2 -1
- schemathesis/cli/handlers.py +4 -1
- schemathesis/cli/junitxml.py +103 -22
- schemathesis/cli/options.py +15 -4
- schemathesis/cli/output/default.py +258 -112
- schemathesis/cli/output/short.py +23 -8
- schemathesis/cli/reporting.py +79 -0
- schemathesis/cli/sanitization.py +6 -0
- schemathesis/code_samples.py +5 -3
- schemathesis/constants.py +1 -0
- schemathesis/contrib/openapi/__init__.py +1 -1
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
- schemathesis/contrib/openapi/formats/uuid.py +2 -1
- schemathesis/contrib/unique_data.py +3 -3
- schemathesis/exceptions.py +76 -65
- schemathesis/experimental/__init__.py +35 -0
- schemathesis/extra/_aiohttp.py +1 -0
- schemathesis/extra/_flask.py +4 -1
- schemathesis/extra/_server.py +1 -0
- schemathesis/extra/pytest_plugin.py +17 -25
- schemathesis/failures.py +77 -9
- schemathesis/filters.py +185 -8
- schemathesis/fixups/__init__.py +1 -0
- schemathesis/fixups/fast_api.py +2 -2
- schemathesis/fixups/utf8_bom.py +1 -2
- schemathesis/generation/__init__.py +20 -36
- schemathesis/generation/_hypothesis.py +59 -0
- schemathesis/generation/_methods.py +44 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/graphql.py +0 -1
- schemathesis/hooks.py +89 -12
- schemathesis/internal/checks.py +84 -0
- schemathesis/internal/copy.py +22 -3
- schemathesis/internal/deprecation.py +6 -2
- schemathesis/internal/diff.py +15 -0
- schemathesis/internal/extensions.py +27 -0
- schemathesis/internal/jsonschema.py +2 -1
- schemathesis/internal/output.py +68 -0
- schemathesis/internal/result.py +1 -1
- schemathesis/internal/transformation.py +11 -0
- schemathesis/lazy.py +138 -25
- schemathesis/loaders.py +7 -5
- schemathesis/models.py +318 -211
- schemathesis/parameters.py +4 -0
- schemathesis/runner/__init__.py +50 -15
- schemathesis/runner/events.py +65 -5
- schemathesis/runner/impl/context.py +104 -0
- schemathesis/runner/impl/core.py +388 -177
- schemathesis/runner/impl/solo.py +19 -29
- schemathesis/runner/impl/threadpool.py +70 -79
- schemathesis/runner/probes.py +11 -9
- schemathesis/runner/serialization.py +150 -17
- schemathesis/sanitization.py +5 -1
- schemathesis/schemas.py +170 -102
- schemathesis/serializers.py +7 -2
- schemathesis/service/ci.py +1 -0
- schemathesis/service/client.py +39 -6
- schemathesis/service/events.py +5 -1
- schemathesis/service/extensions.py +224 -0
- schemathesis/service/hosts.py +6 -2
- schemathesis/service/metadata.py +25 -0
- schemathesis/service/models.py +211 -2
- schemathesis/service/report.py +6 -6
- schemathesis/service/serialization.py +45 -71
- schemathesis/service/usage.py +1 -0
- schemathesis/specs/graphql/_cache.py +26 -0
- schemathesis/specs/graphql/loaders.py +25 -5
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +2 -2
- schemathesis/specs/graphql/schemas.py +130 -100
- schemathesis/specs/graphql/validation.py +1 -2
- schemathesis/specs/openapi/__init__.py +1 -0
- schemathesis/specs/openapi/_cache.py +123 -0
- schemathesis/specs/openapi/_hypothesis.py +78 -60
- schemathesis/specs/openapi/checks.py +504 -25
- schemathesis/specs/openapi/converter.py +31 -4
- schemathesis/specs/openapi/definitions.py +10 -17
- schemathesis/specs/openapi/examples.py +126 -12
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +1 -1
- schemathesis/specs/openapi/expressions/extractors.py +26 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +29 -6
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +44 -0
- schemathesis/specs/openapi/links.py +125 -42
- schemathesis/specs/openapi/loaders.py +77 -36
- schemathesis/specs/openapi/media_types.py +34 -0
- schemathesis/specs/openapi/negative/__init__.py +6 -3
- schemathesis/specs/openapi/negative/mutations.py +21 -6
- schemathesis/specs/openapi/parameters.py +39 -25
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +37 -7
- schemathesis/specs/openapi/schemas.py +360 -241
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +1 -0
- schemathesis/specs/openapi/stateful/__init__.py +198 -70
- schemathesis/specs/openapi/stateful/statistic.py +198 -0
- schemathesis/specs/openapi/stateful/types.py +14 -0
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/specs/openapi/validation.py +1 -0
- schemathesis/stateful/__init__.py +35 -21
- schemathesis/stateful/config.py +97 -0
- schemathesis/stateful/context.py +135 -0
- schemathesis/stateful/events.py +274 -0
- schemathesis/stateful/runner.py +309 -0
- schemathesis/stateful/sink.py +68 -0
- schemathesis/stateful/state_machine.py +67 -38
- schemathesis/stateful/statistic.py +22 -0
- schemathesis/stateful/validation.py +100 -0
- schemathesis/targets.py +33 -1
- schemathesis/throttling.py +25 -5
- schemathesis/transports/__init__.py +354 -0
- schemathesis/transports/asgi.py +7 -0
- schemathesis/transports/auth.py +25 -2
- schemathesis/transports/content_types.py +3 -1
- schemathesis/transports/headers.py +2 -1
- schemathesis/transports/responses.py +9 -4
- schemathesis/types.py +9 -0
- schemathesis/utils.py +11 -16
- schemathesis-3.39.7.dist-info/METADATA +293 -0
- schemathesis-3.39.7.dist-info/RECORD +160 -0
- {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis-3.25.6.dist-info/METADATA +0 -356
- schemathesis-3.25.6.dist-info/RECORD +0 -134
- {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import base64
|
|
4
3
|
import os
|
|
5
4
|
import platform
|
|
6
5
|
import shutil
|
|
7
6
|
import textwrap
|
|
8
7
|
import time
|
|
9
8
|
from importlib import metadata
|
|
10
|
-
from
|
|
11
|
-
from
|
|
12
|
-
from typing import Any, Generator, cast
|
|
9
|
+
from types import GeneratorType
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Generator, Literal, cast
|
|
13
11
|
|
|
14
12
|
import click
|
|
15
13
|
|
|
16
|
-
from ... import service
|
|
17
|
-
from ...code_samples import CodeSampleStyle
|
|
14
|
+
from ... import experimental, service
|
|
18
15
|
from ...constants import (
|
|
19
16
|
DISCORD_LINK,
|
|
20
17
|
FALSE_VALUES,
|
|
@@ -25,17 +22,33 @@ from ...constants import (
|
|
|
25
22
|
SCHEMATHESIS_TEST_CASE_HEADER,
|
|
26
23
|
SCHEMATHESIS_VERSION,
|
|
27
24
|
)
|
|
28
|
-
from ...exceptions import
|
|
25
|
+
from ...exceptions import (
|
|
26
|
+
RuntimeErrorType,
|
|
27
|
+
extract_requests_exception_details,
|
|
28
|
+
format_exception,
|
|
29
|
+
)
|
|
29
30
|
from ...experimental import GLOBAL_EXPERIMENTS
|
|
31
|
+
from ...internal.output import prepare_response_payload
|
|
32
|
+
from ...internal.result import Ok
|
|
30
33
|
from ...models import Status
|
|
31
34
|
from ...runner import events
|
|
32
35
|
from ...runner.events import InternalErrorType, SchemaErrorType
|
|
33
36
|
from ...runner.probes import ProbeOutcome
|
|
34
|
-
from ...runner.serialization import
|
|
37
|
+
from ...runner.serialization import SerializedError, SerializedTestResult
|
|
38
|
+
from ...service.models import AnalysisSuccess, ErrorState, UnknownExtension
|
|
39
|
+
from ...stateful import events as stateful_events
|
|
40
|
+
from ...stateful.sink import StateMachineSink
|
|
35
41
|
from ..context import ExecutionContext, FileReportContext, ServiceReportContext
|
|
36
42
|
from ..handlers import EventHandler
|
|
43
|
+
from ..reporting import TEST_CASE_ID_TITLE, get_runtime_error_suggestion, group_by_case, split_traceback
|
|
44
|
+
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
from queue import Queue
|
|
47
|
+
|
|
48
|
+
import requests
|
|
37
49
|
|
|
38
50
|
SPINNER_REPETITION_NUMBER = 10
|
|
51
|
+
IO_ENCODING = os.getenv("PYTHONIOENCODING", "utf-8")
|
|
39
52
|
|
|
40
53
|
|
|
41
54
|
def get_terminal_width() -> int:
|
|
@@ -60,14 +73,14 @@ def get_percentage(position: int, length: int) -> str:
|
|
|
60
73
|
return f"[{percentage_message}]"
|
|
61
74
|
|
|
62
75
|
|
|
63
|
-
def display_execution_result(context: ExecutionContext,
|
|
76
|
+
def display_execution_result(context: ExecutionContext, status: Literal["success", "failure", "error", "skip"]) -> None:
|
|
64
77
|
"""Display an appropriate symbol for the given event's execution result."""
|
|
65
78
|
symbol, color = {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}[
|
|
79
|
+
"success": (".", "green"),
|
|
80
|
+
"failure": ("F", "red"),
|
|
81
|
+
"error": ("E", "red"),
|
|
82
|
+
"skip": ("S", "yellow"),
|
|
83
|
+
}[status]
|
|
71
84
|
context.current_line_length += len(symbol)
|
|
72
85
|
click.secho(symbol, nl=False, fg=color)
|
|
73
86
|
|
|
@@ -132,7 +145,9 @@ def display_hypothesis_output(hypothesis_output: list[str]) -> None:
|
|
|
132
145
|
|
|
133
146
|
def display_errors(context: ExecutionContext, event: events.Finished) -> None:
|
|
134
147
|
"""Display all errors in the test run."""
|
|
135
|
-
|
|
148
|
+
probes = context.probes or []
|
|
149
|
+
has_probe_errors = any(probe.outcome == ProbeOutcome.ERROR for probe in probes)
|
|
150
|
+
if not event.has_errors and not has_probe_errors:
|
|
136
151
|
return
|
|
137
152
|
|
|
138
153
|
display_section_name("ERRORS")
|
|
@@ -149,6 +164,12 @@ def display_errors(context: ExecutionContext, event: events.Finished) -> None:
|
|
|
149
164
|
should_display_full_traceback_message |= display_single_error(context, result)
|
|
150
165
|
if event.generic_errors:
|
|
151
166
|
display_generic_errors(context, event.generic_errors)
|
|
167
|
+
if has_probe_errors:
|
|
168
|
+
display_section_name("API Probe errors", "_", fg="red")
|
|
169
|
+
for probe in probes:
|
|
170
|
+
if probe.error is not None:
|
|
171
|
+
error = SerializedError.from_exception(probe.error)
|
|
172
|
+
_display_error(context, error)
|
|
152
173
|
if should_display_full_traceback_message and not context.show_trace:
|
|
153
174
|
click.secho(
|
|
154
175
|
"\nAdd this option to your command line parameters to see full tracebacks: --show-trace",
|
|
@@ -181,16 +202,20 @@ def display_generic_errors(context: ExecutionContext, errors: list[SerializedErr
|
|
|
181
202
|
|
|
182
203
|
def display_full_traceback_message(error: SerializedError) -> bool:
|
|
183
204
|
# 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
|
-
|
|
205
|
+
return (
|
|
206
|
+
not error.exception.startswith(
|
|
207
|
+
(
|
|
208
|
+
"DeadlineExceeded",
|
|
209
|
+
"OperationSchemaError",
|
|
210
|
+
"requests.exceptions",
|
|
211
|
+
"SerializationNotPossible",
|
|
212
|
+
"hypothesis.errors.FailedHealthCheck",
|
|
213
|
+
"hypothesis.errors.InvalidArgument: Scalar ",
|
|
214
|
+
"hypothesis.errors.InvalidArgument: min_size=",
|
|
215
|
+
"hypothesis.errors.Unsatisfiable",
|
|
216
|
+
)
|
|
193
217
|
)
|
|
218
|
+
and "can never generate an example, because min_size is larger than Hypothesis supports." not in error.exception
|
|
194
219
|
)
|
|
195
220
|
|
|
196
221
|
|
|
@@ -243,11 +268,11 @@ def _display_error(context: ExecutionContext, error: SerializedError) -> bool:
|
|
|
243
268
|
if error.extras:
|
|
244
269
|
extras = error.extras
|
|
245
270
|
elif context.show_trace and error.type.has_useful_traceback:
|
|
246
|
-
extras =
|
|
271
|
+
extras = split_traceback(error.exception_with_traceback)
|
|
247
272
|
else:
|
|
248
273
|
extras = []
|
|
249
274
|
_display_extras(extras)
|
|
250
|
-
suggestion =
|
|
275
|
+
suggestion = get_runtime_error_suggestion(error.type)
|
|
251
276
|
_maybe_display_tip(suggestion)
|
|
252
277
|
return display_full_traceback_message(error)
|
|
253
278
|
|
|
@@ -266,7 +291,16 @@ def display_failures(context: ExecutionContext, event: events.Finished) -> None:
|
|
|
266
291
|
display_failures_for_single_test(context, result)
|
|
267
292
|
|
|
268
293
|
|
|
269
|
-
|
|
294
|
+
if IO_ENCODING != "utf-8":
|
|
295
|
+
|
|
296
|
+
def _secho(text: str, **kwargs: Any) -> None:
|
|
297
|
+
text = text.encode(IO_ENCODING, errors="replace").decode("utf-8")
|
|
298
|
+
click.secho(text, **kwargs)
|
|
299
|
+
|
|
300
|
+
else:
|
|
301
|
+
|
|
302
|
+
def _secho(text: str, **kwargs: Any) -> None:
|
|
303
|
+
click.secho(text, **kwargs)
|
|
270
304
|
|
|
271
305
|
|
|
272
306
|
def display_failures_for_single_test(context: ExecutionContext, result: SerializedTestResult) -> None:
|
|
@@ -284,63 +318,31 @@ def display_failures_for_single_test(context: ExecutionContext, result: Serializ
|
|
|
284
318
|
for check_idx, check in enumerate(checks):
|
|
285
319
|
if check_idx == 0:
|
|
286
320
|
click.secho(f"{idx}. {TEST_CASE_ID_TITLE}: {check.example.id}", bold=True)
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if check.context.message:
|
|
290
|
-
message = check.context.message
|
|
291
|
-
else:
|
|
292
|
-
message = None
|
|
293
|
-
else:
|
|
294
|
-
title = f"Custom check failed: `{check.name}`"
|
|
295
|
-
message = check.message
|
|
296
|
-
click.secho(f"\n- {title}", fg="red", bold=True)
|
|
321
|
+
click.secho(f"\n- {check.title}", fg="red", bold=True)
|
|
322
|
+
message = check.formatted_message
|
|
297
323
|
if message:
|
|
298
|
-
|
|
299
|
-
click.secho(f"\n{message}", fg="red")
|
|
324
|
+
_secho(f"\n{message}", fg="red")
|
|
300
325
|
if check_idx + 1 == len(checks):
|
|
301
326
|
if check.response is not None:
|
|
302
327
|
status_code = check.response.status_code
|
|
303
328
|
reason = get_reason(status_code)
|
|
304
329
|
response = bold(f"[{check.response.status_code}] {reason}")
|
|
305
330
|
click.echo(f"\n{response}:")
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if check.response is not None and response_body is not None:
|
|
309
|
-
if not response_body:
|
|
331
|
+
if check.response.body is not None:
|
|
332
|
+
if not check.response.body:
|
|
310
333
|
click.echo("\n <EMPTY>")
|
|
311
334
|
else:
|
|
312
335
|
encoding = check.response.encoding or "utf8"
|
|
313
336
|
try:
|
|
314
|
-
|
|
315
|
-
|
|
337
|
+
# Checked that is not None
|
|
338
|
+
body = cast(bytes, check.response.deserialize_body())
|
|
339
|
+
payload = body.decode(encoding)
|
|
340
|
+
payload = prepare_response_payload(payload, config=context.output_config)
|
|
316
341
|
payload = textwrap.indent(f"\n`{payload}`", prefix=" ")
|
|
317
342
|
click.echo(payload)
|
|
318
343
|
except UnicodeDecodeError:
|
|
319
344
|
click.echo("\n <BINARY>")
|
|
320
|
-
|
|
321
|
-
click.echo(
|
|
322
|
-
f"\n{bold('Reproduce with')}: \n\n {code_sample}\n",
|
|
323
|
-
)
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
def group_by_case(
|
|
327
|
-
checks: list[SerializedCheck], code_sample_style: CodeSampleStyle
|
|
328
|
-
) -> Generator[tuple[str, Generator[SerializedCheck, None, None]], None, None]:
|
|
329
|
-
checks = deduplicate_failures(checks)
|
|
330
|
-
checks = sorted(checks, key=lambda c: _by_unique_code_sample(c, code_sample_style))
|
|
331
|
-
yield from groupby(checks, lambda c: _by_unique_code_sample(c, code_sample_style))
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
def _by_unique_code_sample(check: SerializedCheck, code_sample_style: CodeSampleStyle) -> str:
|
|
335
|
-
request_body = base64.b64decode(check.example.body).decode() if check.example.body is not None else None
|
|
336
|
-
return code_sample_style.generate(
|
|
337
|
-
method=check.example.method,
|
|
338
|
-
url=check.example.url,
|
|
339
|
-
body=request_body,
|
|
340
|
-
headers=check.example.headers,
|
|
341
|
-
verify=check.example.verify,
|
|
342
|
-
extra_headers=check.example.extra_headers,
|
|
343
|
-
)
|
|
345
|
+
_secho(f"\n{bold('Reproduce with')}: \n\n {code_sample}\n")
|
|
344
346
|
|
|
345
347
|
|
|
346
348
|
def display_application_logs(context: ExecutionContext, event: events.Finished) -> None:
|
|
@@ -359,11 +361,89 @@ def display_single_log(result: SerializedTestResult) -> None:
|
|
|
359
361
|
click.echo("\n\n".join(result.logs))
|
|
360
362
|
|
|
361
363
|
|
|
364
|
+
def display_analysis(context: ExecutionContext) -> None:
|
|
365
|
+
"""Display schema analysis details."""
|
|
366
|
+
import requests.exceptions
|
|
367
|
+
|
|
368
|
+
if context.analysis is None:
|
|
369
|
+
return
|
|
370
|
+
display_section_name("SCHEMA ANALYSIS")
|
|
371
|
+
if isinstance(context.analysis, Ok):
|
|
372
|
+
analysis = context.analysis.ok()
|
|
373
|
+
click.echo()
|
|
374
|
+
if isinstance(analysis, AnalysisSuccess):
|
|
375
|
+
click.secho(analysis.message, bold=True)
|
|
376
|
+
click.echo(f"\nAnalysis took: {analysis.elapsed:.2f}ms")
|
|
377
|
+
if analysis.extensions:
|
|
378
|
+
known = []
|
|
379
|
+
failed = []
|
|
380
|
+
unknown = []
|
|
381
|
+
for extension in analysis.extensions:
|
|
382
|
+
if isinstance(extension, UnknownExtension):
|
|
383
|
+
unknown.append(extension)
|
|
384
|
+
elif isinstance(extension.state, ErrorState):
|
|
385
|
+
failed.append(extension)
|
|
386
|
+
else:
|
|
387
|
+
known.append(extension)
|
|
388
|
+
if known:
|
|
389
|
+
click.echo("\nThe following extensions have been applied:\n")
|
|
390
|
+
for extension in known:
|
|
391
|
+
click.echo(f" - {extension.summary}")
|
|
392
|
+
if failed:
|
|
393
|
+
click.echo("\nThe following extensions errored:\n")
|
|
394
|
+
for extension in failed:
|
|
395
|
+
click.echo(f" - {extension.summary}")
|
|
396
|
+
suggestion = f"Please, consider reporting this to our issue tracker:\n\n {ISSUE_TRACKER_URL}"
|
|
397
|
+
click.secho(f"\n{click.style('Tip:', bold=True, fg='green')} {suggestion}")
|
|
398
|
+
if unknown:
|
|
399
|
+
noun = "extension" if len(unknown) == 1 else "extensions"
|
|
400
|
+
specific_noun = "this extension" if len(unknown) == 1 else "these extensions"
|
|
401
|
+
title = click.style("Compatibility Notice", bold=True)
|
|
402
|
+
click.secho(f"\n{title}: {len(unknown)} {noun} not recognized:\n")
|
|
403
|
+
for extension in unknown:
|
|
404
|
+
click.echo(f" - {extension.summary}")
|
|
405
|
+
suggestion = f"Consider updating the CLI to add support for {specific_noun}."
|
|
406
|
+
click.secho(f"\n{click.style('Tip:', bold=True, fg='green')} {suggestion}")
|
|
407
|
+
else:
|
|
408
|
+
click.echo("\nNo extensions have been applied.")
|
|
409
|
+
else:
|
|
410
|
+
click.echo("An error happened during schema analysis:\n")
|
|
411
|
+
click.secho(f" {analysis.message}", bold=True)
|
|
412
|
+
click.echo()
|
|
413
|
+
else:
|
|
414
|
+
exception = context.analysis.err()
|
|
415
|
+
suggestion = None
|
|
416
|
+
if isinstance(exception, requests.exceptions.HTTPError):
|
|
417
|
+
response = exception.response
|
|
418
|
+
click.secho("Error\n", fg="red", bold=True)
|
|
419
|
+
_display_service_network_error(response)
|
|
420
|
+
click.echo()
|
|
421
|
+
return
|
|
422
|
+
if isinstance(exception, requests.RequestException):
|
|
423
|
+
message, extras = extract_requests_exception_details(exception)
|
|
424
|
+
suggestion = "Please check your network connection and try again."
|
|
425
|
+
title = "Network Error"
|
|
426
|
+
else:
|
|
427
|
+
traceback = format_exception(exception, True)
|
|
428
|
+
extras = split_traceback(traceback)
|
|
429
|
+
title = "Internal Error"
|
|
430
|
+
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}"
|
|
431
|
+
suggestion = "Please update your CLI to the latest version and try again."
|
|
432
|
+
click.secho(f"{title}\n", fg="red", bold=True)
|
|
433
|
+
click.echo(message)
|
|
434
|
+
_display_extras(extras)
|
|
435
|
+
_maybe_display_tip(suggestion)
|
|
436
|
+
click.echo()
|
|
437
|
+
|
|
438
|
+
|
|
362
439
|
def display_statistic(context: ExecutionContext, event: events.Finished) -> None:
|
|
363
440
|
"""Format and print statistic collected by :obj:`models.TestResult`."""
|
|
364
441
|
display_section_name("SUMMARY")
|
|
365
442
|
click.echo()
|
|
366
443
|
total = event.total
|
|
444
|
+
if context.state_machine_sink is not None:
|
|
445
|
+
click.echo(context.state_machine_sink.transitions.to_formatted_table(get_terminal_width()))
|
|
446
|
+
click.echo()
|
|
367
447
|
if event.is_empty or not total:
|
|
368
448
|
click.secho("No checks were performed.", bold=True)
|
|
369
449
|
|
|
@@ -471,7 +551,7 @@ def display_report_metadata(meta: service.Metadata) -> None:
|
|
|
471
551
|
if value is not None:
|
|
472
552
|
click.secho(f" -> {key}: {value}")
|
|
473
553
|
click.echo()
|
|
474
|
-
click.secho(f"Compressed report size: {meta.size / 1024
|
|
554
|
+
click.secho(f"Compressed report size: {meta.size / 1024.0:,.0f} KB", bold=True)
|
|
475
555
|
|
|
476
556
|
|
|
477
557
|
def display_service_unauthorized(hostname: str) -> None:
|
|
@@ -493,39 +573,45 @@ def display_service_error(event: service.Error, message_prefix: str = "") -> Non
|
|
|
493
573
|
|
|
494
574
|
if isinstance(event.exception, HTTPError):
|
|
495
575
|
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
|
-
)
|
|
576
|
+
_display_service_network_error(response, message_prefix)
|
|
523
577
|
elif isinstance(event.exception, RequestException):
|
|
524
578
|
ask_to_report(event, report_to_issues=False)
|
|
525
579
|
else:
|
|
526
580
|
ask_to_report(event)
|
|
527
581
|
|
|
528
582
|
|
|
583
|
+
def _display_service_network_error(response: requests.Response, message_prefix: str = "") -> None:
|
|
584
|
+
status_code = response.status_code
|
|
585
|
+
if 500 <= status_code <= 599:
|
|
586
|
+
click.secho(f"Schemathesis.io responded with HTTP {status_code}", fg="red")
|
|
587
|
+
# Server error, should be resolved soon
|
|
588
|
+
click.secho(
|
|
589
|
+
"\nIt is likely that we are already notified about the issue and working on a fix\n"
|
|
590
|
+
"Please, try again in 30 minutes",
|
|
591
|
+
fg="red",
|
|
592
|
+
)
|
|
593
|
+
elif status_code == 401:
|
|
594
|
+
# Likely an invalid token
|
|
595
|
+
click.echo("Your CLI is not authenticated.")
|
|
596
|
+
display_service_unauthorized("schemathesis.io")
|
|
597
|
+
else:
|
|
598
|
+
try:
|
|
599
|
+
data = response.json()
|
|
600
|
+
detail = data["detail"]
|
|
601
|
+
click.secho(f"{message_prefix}{detail}", fg="red")
|
|
602
|
+
except Exception:
|
|
603
|
+
# Other client-side errors are likely caused by a bug on the CLI side
|
|
604
|
+
click.secho(
|
|
605
|
+
"We apologize for the inconvenience. This appears to be an internal issue.\n"
|
|
606
|
+
"Please, consider reporting the following details to our issue "
|
|
607
|
+
f"tracker:\n\n {ISSUE_TRACKER_URL}\n\nResponse: {response.text!r}\n"
|
|
608
|
+
f"Status: {response.status_code}\n"
|
|
609
|
+
f"Headers: {response.headers!r}",
|
|
610
|
+
fg="red",
|
|
611
|
+
)
|
|
612
|
+
_maybe_display_tip("Please update your CLI to the latest version and try again.")
|
|
613
|
+
|
|
614
|
+
|
|
529
615
|
SERVICE_ERROR_MESSAGE = "An error happened during uploading reports to Schemathesis.io"
|
|
530
616
|
|
|
531
617
|
|
|
@@ -564,7 +650,6 @@ def wait_for_report_handler(queue: Queue, title: str, timeout: float = service.W
|
|
|
564
650
|
|
|
565
651
|
def create_spinner(repetitions: int) -> Generator[str, None, None]:
|
|
566
652
|
"""A simple spinner that yields its individual characters."""
|
|
567
|
-
assert repetitions > 0, "The number of repetitions should be greater than zero"
|
|
568
653
|
while True:
|
|
569
654
|
for ch in "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏":
|
|
570
655
|
# Skip branch coverage, as it is not possible because of the assertion above
|
|
@@ -625,10 +710,6 @@ def should_skip_suggestion(context: ExecutionContext, event: events.InternalErro
|
|
|
625
710
|
return event.subtype == SchemaErrorType.CONNECTION_OTHER and context.wait_for_schema is not None
|
|
626
711
|
|
|
627
712
|
|
|
628
|
-
def _split_traceback(traceback: str) -> list[str]:
|
|
629
|
-
return [entry for entry in traceback.splitlines() if entry]
|
|
630
|
-
|
|
631
|
-
|
|
632
713
|
def _display_extras(extras: list[str]) -> None:
|
|
633
714
|
if extras:
|
|
634
715
|
click.echo()
|
|
@@ -649,7 +730,7 @@ def display_internal_error(context: ExecutionContext, event: events.InternalErro
|
|
|
649
730
|
if event.type == InternalErrorType.SCHEMA:
|
|
650
731
|
extras = event.extras
|
|
651
732
|
elif context.show_trace:
|
|
652
|
-
extras =
|
|
733
|
+
extras = split_traceback(event.exception_with_traceback)
|
|
653
734
|
else:
|
|
654
735
|
extras = [event.exception]
|
|
655
736
|
_display_extras(extras)
|
|
@@ -696,6 +777,8 @@ def handle_initialized(context: ExecutionContext, event: events.Initialized) ->
|
|
|
696
777
|
click.secho(f"Collected API links: {links_count}", bold=True)
|
|
697
778
|
if isinstance(context.report, ServiceReportContext):
|
|
698
779
|
click.secho("Report to Schemathesis.io: ENABLED", bold=True)
|
|
780
|
+
if context.initialization_lines:
|
|
781
|
+
_print_lines(context.initialization_lines)
|
|
699
782
|
|
|
700
783
|
|
|
701
784
|
def handle_before_probing(context: ExecutionContext, event: events.BeforeProbing) -> None:
|
|
@@ -712,7 +795,23 @@ def handle_after_probing(context: ExecutionContext, event: events.AfterProbing)
|
|
|
712
795
|
status = "SUCCESS"
|
|
713
796
|
elif probe.outcome == ProbeOutcome.ERROR:
|
|
714
797
|
status = "ERROR"
|
|
715
|
-
click.secho(f"API probing: {status}
|
|
798
|
+
click.secho(f"API probing: {status}", bold=True, nl=False)
|
|
799
|
+
click.echo()
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def handle_before_analysis(context: ExecutionContext, event: events.BeforeAnalysis) -> None:
|
|
803
|
+
click.secho("Schema analysis: ...\r", bold=True, nl=False)
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def handle_after_analysis(context: ExecutionContext, event: events.AfterAnalysis) -> None:
|
|
807
|
+
context.analysis = event.analysis
|
|
808
|
+
status = "SKIP"
|
|
809
|
+
if event.analysis is not None:
|
|
810
|
+
if isinstance(event.analysis, Ok) and isinstance(event.analysis.ok(), AnalysisSuccess):
|
|
811
|
+
status = "SUCCESS"
|
|
812
|
+
else:
|
|
813
|
+
status = "ERROR"
|
|
814
|
+
click.secho(f"Schema analysis: {status}", bold=True, nl=False)
|
|
716
815
|
click.echo()
|
|
717
816
|
operations_count = cast(int, context.operations_count) # INVARIANT: should not be `None`
|
|
718
817
|
if operations_count >= 1:
|
|
@@ -741,7 +840,7 @@ def handle_after_execution(context: ExecutionContext, event: events.AfterExecuti
|
|
|
741
840
|
"""Display the execution result + current progress at the same line with the method / path names."""
|
|
742
841
|
context.operations_processed += 1
|
|
743
842
|
context.results.append(event.result)
|
|
744
|
-
display_execution_result(context, event)
|
|
843
|
+
display_execution_result(context, event.status.value)
|
|
745
844
|
display_percentage(context, event)
|
|
746
845
|
|
|
747
846
|
|
|
@@ -752,13 +851,30 @@ def handle_finished(context: ExecutionContext, event: events.Finished) -> None:
|
|
|
752
851
|
display_errors(context, event)
|
|
753
852
|
display_failures(context, event)
|
|
754
853
|
display_application_logs(context, event)
|
|
854
|
+
display_analysis(context)
|
|
755
855
|
display_statistic(context, event)
|
|
856
|
+
if context.summary_lines:
|
|
857
|
+
click.echo()
|
|
858
|
+
_print_lines(context.summary_lines)
|
|
756
859
|
click.echo()
|
|
757
860
|
display_summary(event)
|
|
758
861
|
|
|
759
862
|
|
|
863
|
+
def _print_lines(lines: list[str | Generator[str, None, None]]) -> None:
|
|
864
|
+
for entry in lines:
|
|
865
|
+
if isinstance(entry, str):
|
|
866
|
+
click.echo(entry)
|
|
867
|
+
elif isinstance(entry, GeneratorType):
|
|
868
|
+
for line in entry:
|
|
869
|
+
click.echo(line)
|
|
870
|
+
|
|
871
|
+
|
|
760
872
|
def handle_interrupted(context: ExecutionContext, event: events.Interrupted) -> None:
|
|
761
873
|
click.echo()
|
|
874
|
+
_handle_interrupted(context)
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def _handle_interrupted(context: ExecutionContext) -> None:
|
|
762
878
|
context.is_interrupted = True
|
|
763
879
|
display_section_name("KeyboardInterrupt", "!", bold=False)
|
|
764
880
|
|
|
@@ -768,23 +884,53 @@ def handle_internal_error(context: ExecutionContext, event: events.InternalError
|
|
|
768
884
|
raise click.Abort
|
|
769
885
|
|
|
770
886
|
|
|
887
|
+
def handle_stateful_event(context: ExecutionContext, event: events.StatefulEvent) -> None:
|
|
888
|
+
if isinstance(event.data, stateful_events.RunStarted):
|
|
889
|
+
context.state_machine_sink = event.data.state_machine.sink()
|
|
890
|
+
if not experimental.STATEFUL_ONLY.is_enabled:
|
|
891
|
+
click.echo()
|
|
892
|
+
click.secho("Stateful tests\n", bold=True)
|
|
893
|
+
elif isinstance(event.data, stateful_events.ScenarioFinished) and not event.data.is_final:
|
|
894
|
+
if event.data.status == stateful_events.ScenarioStatus.INTERRUPTED:
|
|
895
|
+
_handle_interrupted(context)
|
|
896
|
+
elif event.data.status != stateful_events.ScenarioStatus.REJECTED:
|
|
897
|
+
display_execution_result(context, event.data.status.value)
|
|
898
|
+
elif isinstance(event.data, stateful_events.RunFinished):
|
|
899
|
+
click.echo()
|
|
900
|
+
# It is initialized in `RunStarted`
|
|
901
|
+
sink = cast(StateMachineSink, context.state_machine_sink)
|
|
902
|
+
sink.consume(event.data)
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def handle_after_stateful_execution(context: ExecutionContext, event: events.AfterStatefulExecution) -> None:
|
|
906
|
+
context.results.append(event.result)
|
|
907
|
+
|
|
908
|
+
|
|
771
909
|
class DefaultOutputStyleHandler(EventHandler):
|
|
772
910
|
def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
|
|
773
911
|
"""Choose and execute a proper handler for the given event."""
|
|
774
912
|
if isinstance(event, events.Initialized):
|
|
775
913
|
handle_initialized(context, event)
|
|
776
|
-
|
|
914
|
+
elif isinstance(event, events.BeforeProbing):
|
|
777
915
|
handle_before_probing(context, event)
|
|
778
|
-
|
|
916
|
+
elif isinstance(event, events.AfterProbing):
|
|
779
917
|
handle_after_probing(context, event)
|
|
780
|
-
|
|
918
|
+
elif isinstance(event, events.BeforeAnalysis):
|
|
919
|
+
handle_before_analysis(context, event)
|
|
920
|
+
elif isinstance(event, events.AfterAnalysis):
|
|
921
|
+
handle_after_analysis(context, event)
|
|
922
|
+
elif isinstance(event, events.BeforeExecution):
|
|
781
923
|
handle_before_execution(context, event)
|
|
782
|
-
|
|
924
|
+
elif isinstance(event, events.AfterExecution):
|
|
783
925
|
context.hypothesis_output.extend(event.hypothesis_output)
|
|
784
926
|
handle_after_execution(context, event)
|
|
785
|
-
|
|
927
|
+
elif isinstance(event, events.Finished):
|
|
786
928
|
handle_finished(context, event)
|
|
787
|
-
|
|
929
|
+
elif isinstance(event, events.Interrupted):
|
|
788
930
|
handle_interrupted(context, event)
|
|
789
|
-
|
|
931
|
+
elif isinstance(event, events.InternalError):
|
|
790
932
|
handle_internal_error(context, event)
|
|
933
|
+
elif isinstance(event, events.StatefulEvent):
|
|
934
|
+
handle_stateful_event(context, event)
|
|
935
|
+
elif isinstance(event, events.AfterStatefulExecution):
|
|
936
|
+
handle_after_stateful_execution(context, event)
|
schemathesis/cli/output/short.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import click
|
|
2
2
|
|
|
3
3
|
from ...runner import events
|
|
4
|
+
from ...stateful import events as stateful_events
|
|
4
5
|
from ..context import ExecutionContext
|
|
5
6
|
from ..handlers import EventHandler
|
|
6
7
|
from . import default
|
|
@@ -15,7 +16,13 @@ def handle_after_execution(context: ExecutionContext, event: events.AfterExecuti
|
|
|
15
16
|
context.operations_processed += 1
|
|
16
17
|
context.results.append(event.result)
|
|
17
18
|
context.hypothesis_output.extend(event.hypothesis_output)
|
|
18
|
-
default.display_execution_result(context, event)
|
|
19
|
+
default.display_execution_result(context, event.status.value)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def handle_stateful_event(context: ExecutionContext, event: events.StatefulEvent) -> None:
|
|
23
|
+
if isinstance(event.data, stateful_events.RunStarted):
|
|
24
|
+
click.echo()
|
|
25
|
+
default.handle_stateful_event(context, event)
|
|
19
26
|
|
|
20
27
|
|
|
21
28
|
class ShortOutputStyleHandler(EventHandler):
|
|
@@ -26,19 +33,27 @@ class ShortOutputStyleHandler(EventHandler):
|
|
|
26
33
|
"""
|
|
27
34
|
if isinstance(event, events.Initialized):
|
|
28
35
|
default.handle_initialized(context, event)
|
|
29
|
-
|
|
36
|
+
elif isinstance(event, events.BeforeProbing):
|
|
30
37
|
default.handle_before_probing(context, event)
|
|
31
|
-
|
|
38
|
+
elif isinstance(event, events.AfterProbing):
|
|
32
39
|
default.handle_after_probing(context, event)
|
|
33
|
-
|
|
40
|
+
elif isinstance(event, events.BeforeAnalysis):
|
|
41
|
+
default.handle_before_analysis(context, event)
|
|
42
|
+
elif isinstance(event, events.AfterAnalysis):
|
|
43
|
+
default.handle_after_analysis(context, event)
|
|
44
|
+
elif isinstance(event, events.BeforeExecution):
|
|
34
45
|
handle_before_execution(context, event)
|
|
35
|
-
|
|
46
|
+
elif isinstance(event, events.AfterExecution):
|
|
36
47
|
handle_after_execution(context, event)
|
|
37
|
-
|
|
48
|
+
elif isinstance(event, events.Finished):
|
|
38
49
|
if context.operations_count == context.operations_processed:
|
|
39
50
|
click.echo()
|
|
40
51
|
default.handle_finished(context, event)
|
|
41
|
-
|
|
52
|
+
elif isinstance(event, events.Interrupted):
|
|
42
53
|
default.handle_interrupted(context, event)
|
|
43
|
-
|
|
54
|
+
elif isinstance(event, events.InternalError):
|
|
44
55
|
default.handle_internal_error(context, event)
|
|
56
|
+
elif isinstance(event, events.StatefulEvent):
|
|
57
|
+
handle_stateful_event(context, event)
|
|
58
|
+
elif isinstance(event, events.AfterStatefulExecution):
|
|
59
|
+
default.handle_after_stateful_execution(context, event)
|