schemathesis 3.25.5__py3-none-any.whl → 3.39.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +6 -6
- schemathesis/_compat.py +2 -2
- schemathesis/_dependency_versions.py +4 -2
- schemathesis/_hypothesis.py +369 -56
- schemathesis/_lazy_import.py +1 -0
- schemathesis/_override.py +5 -4
- schemathesis/_patches.py +21 -0
- schemathesis/_rate_limiter.py +7 -0
- schemathesis/_xml.py +75 -22
- schemathesis/auths.py +78 -16
- schemathesis/checks.py +21 -9
- schemathesis/cli/__init__.py +793 -448
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/callbacks.py +58 -13
- schemathesis/cli/cassettes.py +233 -47
- schemathesis/cli/constants.py +8 -2
- schemathesis/cli/context.py +24 -4
- schemathesis/cli/debug.py +2 -1
- schemathesis/cli/handlers.py +4 -1
- schemathesis/cli/junitxml.py +103 -22
- schemathesis/cli/options.py +15 -4
- schemathesis/cli/output/default.py +286 -115
- schemathesis/cli/output/short.py +25 -6
- schemathesis/cli/reporting.py +79 -0
- schemathesis/cli/sanitization.py +6 -0
- schemathesis/code_samples.py +5 -3
- schemathesis/constants.py +1 -0
- schemathesis/contrib/openapi/__init__.py +1 -1
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
- schemathesis/contrib/openapi/formats/uuid.py +2 -1
- schemathesis/contrib/unique_data.py +3 -3
- schemathesis/exceptions.py +76 -65
- schemathesis/experimental/__init__.py +35 -0
- schemathesis/extra/_aiohttp.py +1 -0
- schemathesis/extra/_flask.py +4 -1
- schemathesis/extra/_server.py +1 -0
- schemathesis/extra/pytest_plugin.py +17 -25
- schemathesis/failures.py +77 -9
- schemathesis/filters.py +185 -8
- schemathesis/fixups/__init__.py +1 -0
- schemathesis/fixups/fast_api.py +2 -2
- schemathesis/fixups/utf8_bom.py +1 -2
- schemathesis/generation/__init__.py +20 -36
- schemathesis/generation/_hypothesis.py +59 -0
- schemathesis/generation/_methods.py +44 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/graphql.py +0 -1
- schemathesis/hooks.py +89 -12
- schemathesis/internal/checks.py +84 -0
- schemathesis/internal/copy.py +22 -3
- schemathesis/internal/deprecation.py +6 -2
- schemathesis/internal/diff.py +15 -0
- schemathesis/internal/extensions.py +27 -0
- schemathesis/internal/jsonschema.py +2 -1
- schemathesis/internal/output.py +68 -0
- schemathesis/internal/result.py +1 -1
- schemathesis/internal/transformation.py +11 -0
- schemathesis/lazy.py +138 -25
- schemathesis/loaders.py +7 -5
- schemathesis/models.py +323 -213
- schemathesis/parameters.py +4 -0
- schemathesis/runner/__init__.py +72 -22
- schemathesis/runner/events.py +86 -6
- schemathesis/runner/impl/context.py +104 -0
- schemathesis/runner/impl/core.py +447 -187
- schemathesis/runner/impl/solo.py +19 -29
- schemathesis/runner/impl/threadpool.py +70 -79
- schemathesis/{cli → runner}/probes.py +37 -25
- schemathesis/runner/serialization.py +150 -17
- schemathesis/sanitization.py +5 -1
- schemathesis/schemas.py +170 -102
- schemathesis/serializers.py +17 -4
- schemathesis/service/ci.py +1 -0
- schemathesis/service/client.py +39 -6
- schemathesis/service/events.py +5 -1
- schemathesis/service/extensions.py +224 -0
- schemathesis/service/hosts.py +6 -2
- schemathesis/service/metadata.py +25 -0
- schemathesis/service/models.py +211 -2
- schemathesis/service/report.py +6 -6
- schemathesis/service/serialization.py +60 -71
- schemathesis/service/usage.py +1 -0
- schemathesis/specs/graphql/_cache.py +26 -0
- schemathesis/specs/graphql/loaders.py +25 -5
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +2 -2
- schemathesis/specs/graphql/schemas.py +130 -100
- schemathesis/specs/graphql/validation.py +1 -2
- schemathesis/specs/openapi/__init__.py +1 -0
- schemathesis/specs/openapi/_cache.py +123 -0
- schemathesis/specs/openapi/_hypothesis.py +79 -61
- schemathesis/specs/openapi/checks.py +504 -25
- schemathesis/specs/openapi/converter.py +31 -4
- schemathesis/specs/openapi/definitions.py +10 -17
- schemathesis/specs/openapi/examples.py +143 -31
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +1 -1
- schemathesis/specs/openapi/expressions/extractors.py +26 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +29 -6
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +44 -0
- schemathesis/specs/openapi/links.py +125 -42
- schemathesis/specs/openapi/loaders.py +77 -36
- schemathesis/specs/openapi/media_types.py +34 -0
- schemathesis/specs/openapi/negative/__init__.py +6 -3
- schemathesis/specs/openapi/negative/mutations.py +21 -6
- schemathesis/specs/openapi/parameters.py +39 -25
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +37 -7
- schemathesis/specs/openapi/schemas.py +368 -242
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +1 -0
- schemathesis/specs/openapi/stateful/__init__.py +198 -70
- schemathesis/specs/openapi/stateful/statistic.py +198 -0
- schemathesis/specs/openapi/stateful/types.py +14 -0
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/specs/openapi/validation.py +1 -0
- schemathesis/stateful/__init__.py +35 -21
- schemathesis/stateful/config.py +97 -0
- schemathesis/stateful/context.py +135 -0
- schemathesis/stateful/events.py +274 -0
- schemathesis/stateful/runner.py +309 -0
- schemathesis/stateful/sink.py +68 -0
- schemathesis/stateful/state_machine.py +67 -38
- schemathesis/stateful/statistic.py +22 -0
- schemathesis/stateful/validation.py +100 -0
- schemathesis/targets.py +33 -1
- schemathesis/throttling.py +25 -5
- schemathesis/transports/__init__.py +354 -0
- schemathesis/transports/asgi.py +7 -0
- schemathesis/transports/auth.py +25 -2
- schemathesis/transports/content_types.py +3 -1
- schemathesis/transports/headers.py +2 -1
- schemathesis/transports/responses.py +9 -4
- schemathesis/types.py +9 -0
- schemathesis/utils.py +11 -16
- schemathesis-3.39.7.dist-info/METADATA +293 -0
- schemathesis-3.39.7.dist-info/RECORD +160 -0
- {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis-3.25.5.dist-info/METADATA +0 -356
- schemathesis-3.25.5.dist-info/RECORD +0 -134
- {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,39 +1,54 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import os
|
|
4
4
|
import platform
|
|
5
5
|
import shutil
|
|
6
6
|
import textwrap
|
|
7
7
|
import time
|
|
8
|
-
from
|
|
9
|
-
from
|
|
10
|
-
from typing import Any, Generator, cast
|
|
8
|
+
from importlib import metadata
|
|
9
|
+
from types import GeneratorType
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Generator, Literal, cast
|
|
11
11
|
|
|
12
12
|
import click
|
|
13
|
-
from importlib import metadata
|
|
14
13
|
|
|
15
|
-
from ... import service
|
|
16
|
-
from ...code_samples import CodeSampleStyle
|
|
14
|
+
from ... import experimental, service
|
|
17
15
|
from ...constants import (
|
|
18
16
|
DISCORD_LINK,
|
|
17
|
+
FALSE_VALUES,
|
|
19
18
|
FLAKY_FAILURE_MESSAGE,
|
|
19
|
+
GITHUB_APP_LINK,
|
|
20
|
+
ISSUE_TRACKER_URL,
|
|
20
21
|
REPORT_SUGGESTION_ENV_VAR,
|
|
21
22
|
SCHEMATHESIS_TEST_CASE_HEADER,
|
|
22
23
|
SCHEMATHESIS_VERSION,
|
|
23
|
-
FALSE_VALUES,
|
|
24
|
-
ISSUE_TRACKER_URL,
|
|
25
|
-
GITHUB_APP_LINK,
|
|
26
24
|
)
|
|
27
|
-
from ...exceptions import
|
|
25
|
+
from ...exceptions import (
|
|
26
|
+
RuntimeErrorType,
|
|
27
|
+
extract_requests_exception_details,
|
|
28
|
+
format_exception,
|
|
29
|
+
)
|
|
28
30
|
from ...experimental import GLOBAL_EXPERIMENTS
|
|
31
|
+
from ...internal.output import prepare_response_payload
|
|
32
|
+
from ...internal.result import Ok
|
|
29
33
|
from ...models import Status
|
|
30
34
|
from ...runner import events
|
|
31
35
|
from ...runner.events import InternalErrorType, SchemaErrorType
|
|
32
|
-
from ...runner.
|
|
36
|
+
from ...runner.probes import ProbeOutcome
|
|
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
|
|
33
41
|
from ..context import ExecutionContext, FileReportContext, ServiceReportContext
|
|
34
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
|
|
35
49
|
|
|
36
50
|
SPINNER_REPETITION_NUMBER = 10
|
|
51
|
+
IO_ENCODING = os.getenv("PYTHONIOENCODING", "utf-8")
|
|
37
52
|
|
|
38
53
|
|
|
39
54
|
def get_terminal_width() -> int:
|
|
@@ -58,14 +73,14 @@ def get_percentage(position: int, length: int) -> str:
|
|
|
58
73
|
return f"[{percentage_message}]"
|
|
59
74
|
|
|
60
75
|
|
|
61
|
-
def display_execution_result(context: ExecutionContext,
|
|
76
|
+
def display_execution_result(context: ExecutionContext, status: Literal["success", "failure", "error", "skip"]) -> None:
|
|
62
77
|
"""Display an appropriate symbol for the given event's execution result."""
|
|
63
78
|
symbol, color = {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}[
|
|
79
|
+
"success": (".", "green"),
|
|
80
|
+
"failure": ("F", "red"),
|
|
81
|
+
"error": ("E", "red"),
|
|
82
|
+
"skip": ("S", "yellow"),
|
|
83
|
+
}[status]
|
|
69
84
|
context.current_line_length += len(symbol)
|
|
70
85
|
click.secho(symbol, nl=False, fg=color)
|
|
71
86
|
|
|
@@ -130,7 +145,9 @@ def display_hypothesis_output(hypothesis_output: list[str]) -> None:
|
|
|
130
145
|
|
|
131
146
|
def display_errors(context: ExecutionContext, event: events.Finished) -> None:
|
|
132
147
|
"""Display all errors in the test run."""
|
|
133
|
-
|
|
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:
|
|
134
151
|
return
|
|
135
152
|
|
|
136
153
|
display_section_name("ERRORS")
|
|
@@ -147,6 +164,12 @@ def display_errors(context: ExecutionContext, event: events.Finished) -> None:
|
|
|
147
164
|
should_display_full_traceback_message |= display_single_error(context, result)
|
|
148
165
|
if event.generic_errors:
|
|
149
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)
|
|
150
173
|
if should_display_full_traceback_message and not context.show_trace:
|
|
151
174
|
click.secho(
|
|
152
175
|
"\nAdd this option to your command line parameters to see full tracebacks: --show-trace",
|
|
@@ -179,16 +202,20 @@ def display_generic_errors(context: ExecutionContext, errors: list[SerializedErr
|
|
|
179
202
|
|
|
180
203
|
def display_full_traceback_message(error: SerializedError) -> bool:
|
|
181
204
|
# 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
|
-
|
|
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
|
+
)
|
|
191
217
|
)
|
|
218
|
+
and "can never generate an example, because min_size is larger than Hypothesis supports." not in error.exception
|
|
192
219
|
)
|
|
193
220
|
|
|
194
221
|
|
|
@@ -241,11 +268,11 @@ def _display_error(context: ExecutionContext, error: SerializedError) -> bool:
|
|
|
241
268
|
if error.extras:
|
|
242
269
|
extras = error.extras
|
|
243
270
|
elif context.show_trace and error.type.has_useful_traceback:
|
|
244
|
-
extras =
|
|
271
|
+
extras = split_traceback(error.exception_with_traceback)
|
|
245
272
|
else:
|
|
246
273
|
extras = []
|
|
247
274
|
_display_extras(extras)
|
|
248
|
-
suggestion =
|
|
275
|
+
suggestion = get_runtime_error_suggestion(error.type)
|
|
249
276
|
_maybe_display_tip(suggestion)
|
|
250
277
|
return display_full_traceback_message(error)
|
|
251
278
|
|
|
@@ -264,7 +291,16 @@ def display_failures(context: ExecutionContext, event: events.Finished) -> None:
|
|
|
264
291
|
display_failures_for_single_test(context, result)
|
|
265
292
|
|
|
266
293
|
|
|
267
|
-
|
|
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)
|
|
268
304
|
|
|
269
305
|
|
|
270
306
|
def display_failures_for_single_test(context: ExecutionContext, result: SerializedTestResult) -> None:
|
|
@@ -282,63 +318,31 @@ def display_failures_for_single_test(context: ExecutionContext, result: Serializ
|
|
|
282
318
|
for check_idx, check in enumerate(checks):
|
|
283
319
|
if check_idx == 0:
|
|
284
320
|
click.secho(f"{idx}. {TEST_CASE_ID_TITLE}: {check.example.id}", bold=True)
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if check.context.message:
|
|
288
|
-
message = check.context.message
|
|
289
|
-
else:
|
|
290
|
-
message = None
|
|
291
|
-
else:
|
|
292
|
-
title = f"Custom check failed: `{check.name}`"
|
|
293
|
-
message = check.message
|
|
294
|
-
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
|
|
295
323
|
if message:
|
|
296
|
-
|
|
297
|
-
click.secho(f"\n{message}", fg="red")
|
|
324
|
+
_secho(f"\n{message}", fg="red")
|
|
298
325
|
if check_idx + 1 == len(checks):
|
|
299
326
|
if check.response is not None:
|
|
300
327
|
status_code = check.response.status_code
|
|
301
328
|
reason = get_reason(status_code)
|
|
302
329
|
response = bold(f"[{check.response.status_code}] {reason}")
|
|
303
330
|
click.echo(f"\n{response}:")
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if check.response is not None and response_body is not None:
|
|
307
|
-
if not response_body:
|
|
331
|
+
if check.response.body is not None:
|
|
332
|
+
if not check.response.body:
|
|
308
333
|
click.echo("\n <EMPTY>")
|
|
309
334
|
else:
|
|
310
335
|
encoding = check.response.encoding or "utf8"
|
|
311
336
|
try:
|
|
312
|
-
|
|
313
|
-
|
|
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)
|
|
314
341
|
payload = textwrap.indent(f"\n`{payload}`", prefix=" ")
|
|
315
342
|
click.echo(payload)
|
|
316
343
|
except UnicodeDecodeError:
|
|
317
344
|
click.echo("\n <BINARY>")
|
|
318
|
-
|
|
319
|
-
click.echo(
|
|
320
|
-
f"\n{bold('Reproduce with')}: \n\n {code_sample}\n",
|
|
321
|
-
)
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
def group_by_case(
|
|
325
|
-
checks: list[SerializedCheck], code_sample_style: CodeSampleStyle
|
|
326
|
-
) -> Generator[tuple[str, Generator[SerializedCheck, None, None]], None, None]:
|
|
327
|
-
checks = deduplicate_failures(checks)
|
|
328
|
-
checks = sorted(checks, key=lambda c: _by_unique_code_sample(c, code_sample_style))
|
|
329
|
-
yield from groupby(checks, lambda c: _by_unique_code_sample(c, code_sample_style))
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
def _by_unique_code_sample(check: SerializedCheck, code_sample_style: CodeSampleStyle) -> str:
|
|
333
|
-
request_body = base64.b64decode(check.example.body).decode() if check.example.body is not None else None
|
|
334
|
-
return code_sample_style.generate(
|
|
335
|
-
method=check.example.method,
|
|
336
|
-
url=check.example.url,
|
|
337
|
-
body=request_body,
|
|
338
|
-
headers=check.example.headers,
|
|
339
|
-
verify=check.example.verify,
|
|
340
|
-
extra_headers=check.example.extra_headers,
|
|
341
|
-
)
|
|
345
|
+
_secho(f"\n{bold('Reproduce with')}: \n\n {code_sample}\n")
|
|
342
346
|
|
|
343
347
|
|
|
344
348
|
def display_application_logs(context: ExecutionContext, event: events.Finished) -> None:
|
|
@@ -357,11 +361,89 @@ def display_single_log(result: SerializedTestResult) -> None:
|
|
|
357
361
|
click.echo("\n\n".join(result.logs))
|
|
358
362
|
|
|
359
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
|
+
|
|
360
439
|
def display_statistic(context: ExecutionContext, event: events.Finished) -> None:
|
|
361
440
|
"""Format and print statistic collected by :obj:`models.TestResult`."""
|
|
362
441
|
display_section_name("SUMMARY")
|
|
363
442
|
click.echo()
|
|
364
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()
|
|
365
447
|
if event.is_empty or not total:
|
|
366
448
|
click.secho("No checks were performed.", bold=True)
|
|
367
449
|
|
|
@@ -469,7 +551,7 @@ def display_report_metadata(meta: service.Metadata) -> None:
|
|
|
469
551
|
if value is not None:
|
|
470
552
|
click.secho(f" -> {key}: {value}")
|
|
471
553
|
click.echo()
|
|
472
|
-
click.secho(f"Compressed report size: {meta.size / 1024
|
|
554
|
+
click.secho(f"Compressed report size: {meta.size / 1024.0:,.0f} KB", bold=True)
|
|
473
555
|
|
|
474
556
|
|
|
475
557
|
def display_service_unauthorized(hostname: str) -> None:
|
|
@@ -487,43 +569,49 @@ def display_service_unauthorized(hostname: str) -> None:
|
|
|
487
569
|
|
|
488
570
|
def display_service_error(event: service.Error, message_prefix: str = "") -> None:
|
|
489
571
|
"""Show information about an error during communication with Schemathesis.io."""
|
|
490
|
-
from requests import
|
|
572
|
+
from requests import HTTPError, RequestException, Response
|
|
491
573
|
|
|
492
574
|
if isinstance(event.exception, HTTPError):
|
|
493
575
|
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
|
-
)
|
|
576
|
+
_display_service_network_error(response, message_prefix)
|
|
521
577
|
elif isinstance(event.exception, RequestException):
|
|
522
578
|
ask_to_report(event, report_to_issues=False)
|
|
523
579
|
else:
|
|
524
580
|
ask_to_report(event)
|
|
525
581
|
|
|
526
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
|
+
|
|
527
615
|
SERVICE_ERROR_MESSAGE = "An error happened during uploading reports to Schemathesis.io"
|
|
528
616
|
|
|
529
617
|
|
|
@@ -562,7 +650,6 @@ def wait_for_report_handler(queue: Queue, title: str, timeout: float = service.W
|
|
|
562
650
|
|
|
563
651
|
def create_spinner(repetitions: int) -> Generator[str, None, None]:
|
|
564
652
|
"""A simple spinner that yields its individual characters."""
|
|
565
|
-
assert repetitions > 0, "The number of repetitions should be greater than zero"
|
|
566
653
|
while True:
|
|
567
654
|
for ch in "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏":
|
|
568
655
|
# Skip branch coverage, as it is not possible because of the assertion above
|
|
@@ -623,10 +710,6 @@ def should_skip_suggestion(context: ExecutionContext, event: events.InternalErro
|
|
|
623
710
|
return event.subtype == SchemaErrorType.CONNECTION_OTHER and context.wait_for_schema is not None
|
|
624
711
|
|
|
625
712
|
|
|
626
|
-
def _split_traceback(traceback: str) -> list[str]:
|
|
627
|
-
return [entry for entry in traceback.splitlines() if entry]
|
|
628
|
-
|
|
629
|
-
|
|
630
713
|
def _display_extras(extras: list[str]) -> None:
|
|
631
714
|
if extras:
|
|
632
715
|
click.echo()
|
|
@@ -647,7 +730,7 @@ def display_internal_error(context: ExecutionContext, event: events.InternalErro
|
|
|
647
730
|
if event.type == InternalErrorType.SCHEMA:
|
|
648
731
|
extras = event.extras
|
|
649
732
|
elif context.show_trace:
|
|
650
|
-
extras =
|
|
733
|
+
extras = split_traceback(event.exception_with_traceback)
|
|
651
734
|
else:
|
|
652
735
|
extras = [event.exception]
|
|
653
736
|
_display_extras(extras)
|
|
@@ -694,7 +777,44 @@ def handle_initialized(context: ExecutionContext, event: events.Initialized) ->
|
|
|
694
777
|
click.secho(f"Collected API links: {links_count}", bold=True)
|
|
695
778
|
if isinstance(context.report, ServiceReportContext):
|
|
696
779
|
click.secho("Report to Schemathesis.io: ENABLED", bold=True)
|
|
697
|
-
if context.
|
|
780
|
+
if context.initialization_lines:
|
|
781
|
+
_print_lines(context.initialization_lines)
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def handle_before_probing(context: ExecutionContext, event: events.BeforeProbing) -> None:
|
|
785
|
+
click.secho("API probing: ...\r", bold=True, nl=False)
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def handle_after_probing(context: ExecutionContext, event: events.AfterProbing) -> None:
|
|
789
|
+
context.probes = event.probes
|
|
790
|
+
status = "SKIP"
|
|
791
|
+
if event.probes is not None:
|
|
792
|
+
for probe in event.probes:
|
|
793
|
+
if probe.outcome in (ProbeOutcome.SUCCESS, ProbeOutcome.FAILURE):
|
|
794
|
+
# The probe itself has been executed
|
|
795
|
+
status = "SUCCESS"
|
|
796
|
+
elif probe.outcome == ProbeOutcome.ERROR:
|
|
797
|
+
status = "ERROR"
|
|
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)
|
|
815
|
+
click.echo()
|
|
816
|
+
operations_count = cast(int, context.operations_count) # INVARIANT: should not be `None`
|
|
817
|
+
if operations_count >= 1:
|
|
698
818
|
click.echo()
|
|
699
819
|
|
|
700
820
|
|
|
@@ -720,7 +840,7 @@ def handle_after_execution(context: ExecutionContext, event: events.AfterExecuti
|
|
|
720
840
|
"""Display the execution result + current progress at the same line with the method / path names."""
|
|
721
841
|
context.operations_processed += 1
|
|
722
842
|
context.results.append(event.result)
|
|
723
|
-
display_execution_result(context, event)
|
|
843
|
+
display_execution_result(context, event.status.value)
|
|
724
844
|
display_percentage(context, event)
|
|
725
845
|
|
|
726
846
|
|
|
@@ -731,13 +851,30 @@ def handle_finished(context: ExecutionContext, event: events.Finished) -> None:
|
|
|
731
851
|
display_errors(context, event)
|
|
732
852
|
display_failures(context, event)
|
|
733
853
|
display_application_logs(context, event)
|
|
854
|
+
display_analysis(context)
|
|
734
855
|
display_statistic(context, event)
|
|
856
|
+
if context.summary_lines:
|
|
857
|
+
click.echo()
|
|
858
|
+
_print_lines(context.summary_lines)
|
|
735
859
|
click.echo()
|
|
736
860
|
display_summary(event)
|
|
737
861
|
|
|
738
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
|
+
|
|
739
872
|
def handle_interrupted(context: ExecutionContext, event: events.Interrupted) -> None:
|
|
740
873
|
click.echo()
|
|
874
|
+
_handle_interrupted(context)
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def _handle_interrupted(context: ExecutionContext) -> None:
|
|
741
878
|
context.is_interrupted = True
|
|
742
879
|
display_section_name("KeyboardInterrupt", "!", bold=False)
|
|
743
880
|
|
|
@@ -747,19 +884,53 @@ def handle_internal_error(context: ExecutionContext, event: events.InternalError
|
|
|
747
884
|
raise click.Abort
|
|
748
885
|
|
|
749
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
|
+
|
|
750
909
|
class DefaultOutputStyleHandler(EventHandler):
|
|
751
910
|
def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
|
|
752
911
|
"""Choose and execute a proper handler for the given event."""
|
|
753
912
|
if isinstance(event, events.Initialized):
|
|
754
913
|
handle_initialized(context, event)
|
|
755
|
-
|
|
914
|
+
elif isinstance(event, events.BeforeProbing):
|
|
915
|
+
handle_before_probing(context, event)
|
|
916
|
+
elif isinstance(event, events.AfterProbing):
|
|
917
|
+
handle_after_probing(context, event)
|
|
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):
|
|
756
923
|
handle_before_execution(context, event)
|
|
757
|
-
|
|
924
|
+
elif isinstance(event, events.AfterExecution):
|
|
758
925
|
context.hypothesis_output.extend(event.hypothesis_output)
|
|
759
926
|
handle_after_execution(context, event)
|
|
760
|
-
|
|
927
|
+
elif isinstance(event, events.Finished):
|
|
761
928
|
handle_finished(context, event)
|
|
762
|
-
|
|
929
|
+
elif isinstance(event, events.Interrupted):
|
|
763
930
|
handle_interrupted(context, event)
|
|
764
|
-
|
|
931
|
+
elif isinstance(event, events.InternalError):
|
|
765
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,15 +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):
|
|
37
|
+
default.handle_before_probing(context, event)
|
|
38
|
+
elif isinstance(event, events.AfterProbing):
|
|
39
|
+
default.handle_after_probing(context, event)
|
|
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):
|
|
30
45
|
handle_before_execution(context, event)
|
|
31
|
-
|
|
46
|
+
elif isinstance(event, events.AfterExecution):
|
|
32
47
|
handle_after_execution(context, event)
|
|
33
|
-
|
|
48
|
+
elif isinstance(event, events.Finished):
|
|
34
49
|
if context.operations_count == context.operations_processed:
|
|
35
50
|
click.echo()
|
|
36
51
|
default.handle_finished(context, event)
|
|
37
|
-
|
|
52
|
+
elif isinstance(event, events.Interrupted):
|
|
38
53
|
default.handle_interrupted(context, event)
|
|
39
|
-
|
|
54
|
+
elif isinstance(event, events.InternalError):
|
|
40
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)
|