schemathesis 3.25.5__py3-none-any.whl → 4.0.0a1__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 +27 -65
- schemathesis/auths.py +102 -82
- schemathesis/checks.py +126 -46
- schemathesis/cli/__init__.py +11 -1766
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +37 -0
- schemathesis/cli/commands/run/__init__.py +662 -0
- schemathesis/cli/commands/run/checks.py +80 -0
- schemathesis/cli/commands/run/context.py +117 -0
- schemathesis/cli/commands/run/events.py +35 -0
- schemathesis/cli/commands/run/executor.py +138 -0
- schemathesis/cli/commands/run/filters.py +194 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
- schemathesis/cli/commands/run/handlers/output.py +746 -0
- schemathesis/cli/commands/run/hypothesis.py +105 -0
- schemathesis/cli/commands/run/loaders.py +129 -0
- schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
- schemathesis/cli/constants.py +5 -52
- schemathesis/cli/core.py +17 -0
- schemathesis/cli/ext/fs.py +14 -0
- schemathesis/cli/ext/groups.py +55 -0
- schemathesis/cli/{options.py → ext/options.py} +39 -10
- schemathesis/cli/hooks.py +36 -0
- schemathesis/contrib/__init__.py +1 -3
- schemathesis/contrib/openapi/__init__.py +1 -3
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
- schemathesis/core/__init__.py +58 -0
- schemathesis/core/compat.py +25 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +370 -0
- schemathesis/core/failures.py +285 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
- schemathesis/core/output/__init__.py +69 -0
- schemathesis/core/output/sanitization.py +197 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +31 -0
- schemathesis/{internal → core}/result.py +1 -1
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +108 -0
- schemathesis/core/validation.py +38 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +30 -0
- schemathesis/engine/config.py +59 -0
- schemathesis/engine/context.py +119 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +157 -0
- schemathesis/engine/errors.py +394 -0
- schemathesis/engine/events.py +337 -0
- schemathesis/engine/phases/__init__.py +66 -0
- schemathesis/{cli → engine/phases}/probes.py +63 -70
- schemathesis/engine/phases/stateful/__init__.py +65 -0
- schemathesis/engine/phases/stateful/_executor.py +326 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +174 -0
- schemathesis/engine/phases/unit/_executor.py +321 -0
- schemathesis/engine/phases/unit/_pool.py +74 -0
- schemathesis/engine/recorder.py +241 -0
- schemathesis/errors.py +31 -0
- schemathesis/experimental/__init__.py +18 -14
- schemathesis/filters.py +103 -14
- schemathesis/generation/__init__.py +21 -37
- schemathesis/generation/case.py +190 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/generation/hypothesis/__init__.py +30 -0
- schemathesis/generation/hypothesis/builder.py +585 -0
- schemathesis/generation/hypothesis/examples.py +50 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/modes.py +28 -0
- schemathesis/generation/overrides.py +96 -0
- schemathesis/generation/stateful/__init__.py +20 -0
- schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
- schemathesis/generation/targets.py +69 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +115 -0
- schemathesis/graphql/loaders.py +131 -0
- schemathesis/hooks.py +99 -67
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +412 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +63 -0
- schemathesis/openapi/loaders.py +178 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +273 -0
- schemathesis/pytest/loaders.py +12 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -261
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +25 -0
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +7 -5
- schemathesis/specs/graphql/schemas.py +215 -187
- schemathesis/specs/graphql/validation.py +11 -18
- schemathesis/specs/openapi/__init__.py +7 -1
- schemathesis/specs/openapi/_cache.py +122 -0
- schemathesis/specs/openapi/_hypothesis.py +146 -165
- schemathesis/specs/openapi/checks.py +565 -67
- schemathesis/specs/openapi/converter.py +33 -6
- schemathesis/specs/openapi/definitions.py +11 -18
- schemathesis/specs/openapi/examples.py +153 -39
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +4 -6
- schemathesis/specs/openapi/expressions/extractors.py +23 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +38 -14
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +45 -0
- schemathesis/specs/openapi/links.py +65 -165
- schemathesis/specs/openapi/media_types.py +32 -0
- schemathesis/specs/openapi/negative/__init__.py +7 -3
- schemathesis/specs/openapi/negative/mutations.py +24 -8
- schemathesis/specs/openapi/parameters.py +46 -30
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +47 -57
- schemathesis/specs/openapi/schemas.py +483 -367
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +11 -6
- schemathesis/specs/openapi/stateful/__init__.py +185 -73
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/transport/__init__.py +104 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +99 -0
- schemathesis/transport/requests.py +221 -0
- schemathesis/{_xml.py → transport/serialization.py} +143 -28
- schemathesis/transport/wsgi.py +165 -0
- schemathesis-4.0.0a1.dist-info/METADATA +297 -0
- schemathesis-4.0.0a1.dist-info/RECORD +152 -0
- {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
- {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -17
- schemathesis/_hypothesis.py +0 -246
- schemathesis/_override.py +0 -49
- schemathesis/cli/cassettes.py +0 -375
- schemathesis/cli/context.py +0 -55
- schemathesis/cli/debug.py +0 -26
- schemathesis/cli/handlers.py +0 -16
- schemathesis/cli/junitxml.py +0 -43
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -765
- schemathesis/cli/output/short.py +0 -40
- schemathesis/cli/sanitization.py +0 -20
- schemathesis/code_samples.py +0 -149
- schemathesis/constants.py +0 -55
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -15
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -560
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -17
- schemathesis/failures.py +0 -209
- schemathesis/fixups/__init__.py +0 -36
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -29
- schemathesis/graphql.py +0 -4
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/copy.py +0 -13
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -34
- schemathesis/internal/jsonschema.py +0 -35
- schemathesis/internal/transformation.py +0 -15
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -361
- schemathesis/loaders.py +0 -120
- schemathesis/models.py +0 -1231
- schemathesis/parameters.py +0 -86
- schemathesis/runner/__init__.py +0 -555
- schemathesis/runner/events.py +0 -309
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -986
- schemathesis/runner/impl/solo.py +0 -90
- schemathesis/runner/impl/threadpool.py +0 -400
- schemathesis/runner/serialization.py +0 -411
- schemathesis/sanitization.py +0 -248
- schemathesis/serializers.py +0 -315
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -201
- schemathesis/service/client.py +0 -100
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -57
- schemathesis/service/hosts.py +0 -107
- schemathesis/service/metadata.py +0 -46
- schemathesis/service/models.py +0 -49
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -184
- schemathesis/service/usage.py +0 -65
- schemathesis/specs/graphql/loaders.py +0 -344
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/loaders.py +0 -667
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis/specs/openapi/validation.py +0 -25
- schemathesis/stateful/__init__.py +0 -133
- schemathesis/targets.py +0 -45
- schemathesis/throttling.py +0 -41
- schemathesis/transports/__init__.py +0 -5
- schemathesis/transports/auth.py +0 -15
- schemathesis/transports/headers.py +0 -35
- schemathesis/transports/responses.py +0 -52
- schemathesis/types.py +0 -35
- schemathesis/utils.py +0 -169
- schemathesis-3.25.5.dist-info/METADATA +0 -356
- schemathesis-3.25.5.dist-info/RECORD +0 -134
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -1,765 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
import base64
|
3
|
-
import os
|
4
|
-
import platform
|
5
|
-
import shutil
|
6
|
-
import textwrap
|
7
|
-
import time
|
8
|
-
from itertools import groupby
|
9
|
-
from queue import Queue
|
10
|
-
from typing import Any, Generator, cast
|
11
|
-
|
12
|
-
import click
|
13
|
-
from importlib import metadata
|
14
|
-
|
15
|
-
from ... import service
|
16
|
-
from ...code_samples import CodeSampleStyle
|
17
|
-
from ...constants import (
|
18
|
-
DISCORD_LINK,
|
19
|
-
FLAKY_FAILURE_MESSAGE,
|
20
|
-
REPORT_SUGGESTION_ENV_VAR,
|
21
|
-
SCHEMATHESIS_TEST_CASE_HEADER,
|
22
|
-
SCHEMATHESIS_VERSION,
|
23
|
-
FALSE_VALUES,
|
24
|
-
ISSUE_TRACKER_URL,
|
25
|
-
GITHUB_APP_LINK,
|
26
|
-
)
|
27
|
-
from ...exceptions import RuntimeErrorType, prepare_response_payload
|
28
|
-
from ...experimental import GLOBAL_EXPERIMENTS
|
29
|
-
from ...models import Status
|
30
|
-
from ...runner import events
|
31
|
-
from ...runner.events import InternalErrorType, SchemaErrorType
|
32
|
-
from ...runner.serialization import SerializedError, SerializedTestResult, deduplicate_failures, SerializedCheck
|
33
|
-
from ..context import ExecutionContext, FileReportContext, ServiceReportContext
|
34
|
-
from ..handlers import EventHandler
|
35
|
-
|
36
|
-
SPINNER_REPETITION_NUMBER = 10
|
37
|
-
|
38
|
-
|
39
|
-
def get_terminal_width() -> int:
|
40
|
-
# Some CI/CD providers (e.g. CircleCI) return a (0, 0) terminal size so provide a default
|
41
|
-
return shutil.get_terminal_size((80, 24)).columns
|
42
|
-
|
43
|
-
|
44
|
-
def display_section_name(title: str, separator: str = "=", **kwargs: Any) -> None:
|
45
|
-
"""Print section name with separators in terminal with the given title nicely centered."""
|
46
|
-
message = f" {title} ".center(get_terminal_width(), separator)
|
47
|
-
kwargs.setdefault("bold", True)
|
48
|
-
click.secho(message, **kwargs)
|
49
|
-
|
50
|
-
|
51
|
-
def display_subsection(result: SerializedTestResult, color: str | None = "red") -> None:
|
52
|
-
display_section_name(result.verbose_name, "_", fg=color)
|
53
|
-
|
54
|
-
|
55
|
-
def get_percentage(position: int, length: int) -> str:
|
56
|
-
"""Format completion percentage in square brackets."""
|
57
|
-
percentage_message = f"{position * 100 // length}%".rjust(4)
|
58
|
-
return f"[{percentage_message}]"
|
59
|
-
|
60
|
-
|
61
|
-
def display_execution_result(context: ExecutionContext, event: events.AfterExecution) -> None:
|
62
|
-
"""Display an appropriate symbol for the given event's execution result."""
|
63
|
-
symbol, color = {
|
64
|
-
Status.success: (".", "green"),
|
65
|
-
Status.failure: ("F", "red"),
|
66
|
-
Status.error: ("E", "red"),
|
67
|
-
Status.skip: ("S", "yellow"),
|
68
|
-
}[event.status]
|
69
|
-
context.current_line_length += len(symbol)
|
70
|
-
click.secho(symbol, nl=False, fg=color)
|
71
|
-
|
72
|
-
|
73
|
-
def display_percentage(context: ExecutionContext, event: events.AfterExecution) -> None:
|
74
|
-
"""Add the current progress in % to the right side of the current line."""
|
75
|
-
operations_count = cast(int, context.operations_count) # is already initialized via `Initialized` event
|
76
|
-
current_percentage = get_percentage(context.operations_processed, operations_count)
|
77
|
-
styled = click.style(current_percentage, fg="cyan")
|
78
|
-
# Total length of the message, so it will fill to the right border of the terminal.
|
79
|
-
# Padding is already taken into account in `context.current_line_length`
|
80
|
-
length = max(get_terminal_width() - context.current_line_length + len(styled) - len(current_percentage), 1)
|
81
|
-
template = f"{{:>{length}}}"
|
82
|
-
click.echo(template.format(styled))
|
83
|
-
|
84
|
-
|
85
|
-
def display_summary(event: events.Finished) -> None:
|
86
|
-
message, color = get_summary_output(event)
|
87
|
-
display_section_name(message, fg=color)
|
88
|
-
|
89
|
-
|
90
|
-
def get_summary_message_parts(event: events.Finished) -> list[str]:
|
91
|
-
parts = []
|
92
|
-
passed = event.passed_count
|
93
|
-
if passed:
|
94
|
-
parts.append(f"{passed} passed")
|
95
|
-
failed = event.failed_count
|
96
|
-
if failed:
|
97
|
-
parts.append(f"{failed} failed")
|
98
|
-
errored = event.errored_count
|
99
|
-
if errored:
|
100
|
-
parts.append(f"{errored} errored")
|
101
|
-
skipped = event.skipped_count
|
102
|
-
if skipped:
|
103
|
-
parts.append(f"{skipped} skipped")
|
104
|
-
return parts
|
105
|
-
|
106
|
-
|
107
|
-
def get_summary_output(event: events.Finished) -> tuple[str, str]:
|
108
|
-
parts = get_summary_message_parts(event)
|
109
|
-
if not parts:
|
110
|
-
message = "Empty test suite"
|
111
|
-
color = "yellow"
|
112
|
-
else:
|
113
|
-
message = f'{", ".join(parts)} in {event.running_time:.2f}s'
|
114
|
-
if event.has_failures or event.has_errors:
|
115
|
-
color = "red"
|
116
|
-
elif event.skipped_count > 0:
|
117
|
-
color = "yellow"
|
118
|
-
else:
|
119
|
-
color = "green"
|
120
|
-
return message, color
|
121
|
-
|
122
|
-
|
123
|
-
def display_hypothesis_output(hypothesis_output: list[str]) -> None:
|
124
|
-
"""Show falsifying examples from Hypothesis output if there are any."""
|
125
|
-
if hypothesis_output:
|
126
|
-
display_section_name("HYPOTHESIS OUTPUT")
|
127
|
-
output = "\n".join(hypothesis_output)
|
128
|
-
click.secho(output, fg="red")
|
129
|
-
|
130
|
-
|
131
|
-
def display_errors(context: ExecutionContext, event: events.Finished) -> None:
|
132
|
-
"""Display all errors in the test run."""
|
133
|
-
if not event.has_errors:
|
134
|
-
return
|
135
|
-
|
136
|
-
display_section_name("ERRORS")
|
137
|
-
should_display_full_traceback_message = False
|
138
|
-
if context.workers_num > 1:
|
139
|
-
# Events may come out of order when multiple workers are involved
|
140
|
-
# Sort them to get a stable output
|
141
|
-
results = sorted(context.results, key=lambda r: r.verbose_name)
|
142
|
-
else:
|
143
|
-
results = context.results
|
144
|
-
for result in results:
|
145
|
-
if not result.has_errors:
|
146
|
-
continue
|
147
|
-
should_display_full_traceback_message |= display_single_error(context, result)
|
148
|
-
if event.generic_errors:
|
149
|
-
display_generic_errors(context, event.generic_errors)
|
150
|
-
if should_display_full_traceback_message and not context.show_trace:
|
151
|
-
click.secho(
|
152
|
-
"\nAdd this option to your command line parameters to see full tracebacks: --show-trace",
|
153
|
-
fg="red",
|
154
|
-
)
|
155
|
-
click.secho(
|
156
|
-
f"\nNeed more help?\n" f" Join our Discord server: {DISCORD_LINK}",
|
157
|
-
fg="red",
|
158
|
-
)
|
159
|
-
|
160
|
-
|
161
|
-
def display_single_error(context: ExecutionContext, result: SerializedTestResult) -> bool:
|
162
|
-
display_subsection(result)
|
163
|
-
should_display_full_traceback_message = False
|
164
|
-
first = True
|
165
|
-
for error in result.errors:
|
166
|
-
if first:
|
167
|
-
first = False
|
168
|
-
else:
|
169
|
-
click.echo()
|
170
|
-
should_display_full_traceback_message |= _display_error(context, error)
|
171
|
-
return should_display_full_traceback_message
|
172
|
-
|
173
|
-
|
174
|
-
def display_generic_errors(context: ExecutionContext, errors: list[SerializedError]) -> None:
|
175
|
-
for error in errors:
|
176
|
-
display_section_name(error.title or "Generic error", "_", fg="red")
|
177
|
-
_display_error(context, error)
|
178
|
-
|
179
|
-
|
180
|
-
def display_full_traceback_message(error: SerializedError) -> bool:
|
181
|
-
# Some errors should not trigger the message that suggests to show full tracebacks to the user
|
182
|
-
return not error.exception.startswith(
|
183
|
-
(
|
184
|
-
"DeadlineExceeded",
|
185
|
-
"OperationSchemaError",
|
186
|
-
"requests.exceptions",
|
187
|
-
"SerializationNotPossible",
|
188
|
-
"hypothesis.errors.FailedHealthCheck",
|
189
|
-
"hypothesis.errors.InvalidArgument: Scalar ",
|
190
|
-
"hypothesis.errors.InvalidArgument: min_size=",
|
191
|
-
)
|
192
|
-
)
|
193
|
-
|
194
|
-
|
195
|
-
def bold(option: str) -> str:
|
196
|
-
return click.style(option, bold=True)
|
197
|
-
|
198
|
-
|
199
|
-
DISABLE_SSL_SUGGESTION = f"Bypass SSL verification with {bold('`--request-tls-verify=false`')}."
|
200
|
-
DISABLE_SCHEMA_VALIDATION_SUGGESTION = (
|
201
|
-
f"Bypass validation using {bold('`--validate-schema=false`')}. Caution: May cause unexpected errors."
|
202
|
-
)
|
203
|
-
|
204
|
-
|
205
|
-
def _format_health_check_suggestion(label: str) -> str:
|
206
|
-
return f"Bypass this health check using {bold(f'`--hypothesis-suppress-health-check={label}`')}."
|
207
|
-
|
208
|
-
|
209
|
-
RUNTIME_ERROR_SUGGESTIONS = {
|
210
|
-
RuntimeErrorType.CONNECTION_SSL: DISABLE_SSL_SUGGESTION,
|
211
|
-
RuntimeErrorType.HYPOTHESIS_DEADLINE_EXCEEDED: (
|
212
|
-
f"Adjust the deadline using {bold('`--hypothesis-deadline=MILLIS`')} or "
|
213
|
-
f"disable with {bold('`--hypothesis-deadline=None`')}."
|
214
|
-
),
|
215
|
-
RuntimeErrorType.HYPOTHESIS_UNSATISFIABLE: "Examine the schema for inconsistencies and consider simplifying it.",
|
216
|
-
RuntimeErrorType.SCHEMA_BODY_IN_GET_REQUEST: DISABLE_SCHEMA_VALIDATION_SUGGESTION,
|
217
|
-
RuntimeErrorType.SCHEMA_INVALID_REGULAR_EXPRESSION: "Ensure your regex is compatible with Python's syntax.\n"
|
218
|
-
"For guidance, visit: https://docs.python.org/3/library/re.html",
|
219
|
-
RuntimeErrorType.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR: "Define a custom strategy for it.\n"
|
220
|
-
"For guidance, visit: https://schemathesis.readthedocs.io/en/stable/graphql.html#custom-scalars",
|
221
|
-
RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE: _format_health_check_suggestion("data_too_large"),
|
222
|
-
RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH: _format_health_check_suggestion("filter_too_much"),
|
223
|
-
RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW: _format_health_check_suggestion("too_slow"),
|
224
|
-
RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE: _format_health_check_suggestion("large_base_example"),
|
225
|
-
}
|
226
|
-
|
227
|
-
|
228
|
-
def _display_error(context: ExecutionContext, error: SerializedError) -> bool:
|
229
|
-
if error.title:
|
230
|
-
if error.type == RuntimeErrorType.SCHEMA_GENERIC:
|
231
|
-
click.secho("Schema Error", fg="red", bold=True)
|
232
|
-
else:
|
233
|
-
click.secho(error.title, fg="red", bold=True)
|
234
|
-
click.echo()
|
235
|
-
if error.message:
|
236
|
-
click.echo(error.message)
|
237
|
-
elif error.message:
|
238
|
-
click.echo(error.message)
|
239
|
-
else:
|
240
|
-
click.echo(error.exception)
|
241
|
-
if error.extras:
|
242
|
-
extras = error.extras
|
243
|
-
elif context.show_trace and error.type.has_useful_traceback:
|
244
|
-
extras = _split_traceback(error.exception_with_traceback)
|
245
|
-
else:
|
246
|
-
extras = []
|
247
|
-
_display_extras(extras)
|
248
|
-
suggestion = RUNTIME_ERROR_SUGGESTIONS.get(error.type)
|
249
|
-
_maybe_display_tip(suggestion)
|
250
|
-
return display_full_traceback_message(error)
|
251
|
-
|
252
|
-
|
253
|
-
def display_failures(context: ExecutionContext, event: events.Finished) -> None:
|
254
|
-
"""Display all failures in the test run."""
|
255
|
-
if not event.has_failures:
|
256
|
-
return
|
257
|
-
relevant_results = [result for result in context.results if not result.is_errored]
|
258
|
-
if not relevant_results:
|
259
|
-
return
|
260
|
-
display_section_name("FAILURES")
|
261
|
-
for result in relevant_results:
|
262
|
-
if not result.has_failures:
|
263
|
-
continue
|
264
|
-
display_failures_for_single_test(context, result)
|
265
|
-
|
266
|
-
|
267
|
-
TEST_CASE_ID_TITLE = "Test Case ID"
|
268
|
-
|
269
|
-
|
270
|
-
def display_failures_for_single_test(context: ExecutionContext, result: SerializedTestResult) -> None:
|
271
|
-
"""Display a failure for a single method / path."""
|
272
|
-
from ...transports.responses import get_reason
|
273
|
-
|
274
|
-
display_subsection(result)
|
275
|
-
if result.is_flaky:
|
276
|
-
click.secho(FLAKY_FAILURE_MESSAGE, fg="red")
|
277
|
-
click.echo()
|
278
|
-
for idx, (code_sample, group) in enumerate(group_by_case(result.checks, context.code_sample_style), 1):
|
279
|
-
# Make server errors appear first in the list of checks
|
280
|
-
checks = sorted(group, key=lambda c: c.name != "not_a_server_error")
|
281
|
-
|
282
|
-
for check_idx, check in enumerate(checks):
|
283
|
-
if check_idx == 0:
|
284
|
-
click.secho(f"{idx}. {TEST_CASE_ID_TITLE}: {check.example.id}", bold=True)
|
285
|
-
if check.context is not None:
|
286
|
-
title = check.context.title
|
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)
|
295
|
-
if message:
|
296
|
-
message = textwrap.indent(message, prefix=" ")
|
297
|
-
click.secho(f"\n{message}", fg="red")
|
298
|
-
if check_idx + 1 == len(checks):
|
299
|
-
if check.response is not None:
|
300
|
-
status_code = check.response.status_code
|
301
|
-
reason = get_reason(status_code)
|
302
|
-
response = bold(f"[{check.response.status_code}] {reason}")
|
303
|
-
click.echo(f"\n{response}:")
|
304
|
-
|
305
|
-
response_body = check.response.body
|
306
|
-
if check.response is not None and response_body is not None:
|
307
|
-
if not response_body:
|
308
|
-
click.echo("\n <EMPTY>")
|
309
|
-
else:
|
310
|
-
encoding = check.response.encoding or "utf8"
|
311
|
-
try:
|
312
|
-
payload = base64.b64decode(response_body).decode(encoding)
|
313
|
-
payload = prepare_response_payload(payload)
|
314
|
-
payload = textwrap.indent(f"\n`{payload}`", prefix=" ")
|
315
|
-
click.echo(payload)
|
316
|
-
except UnicodeDecodeError:
|
317
|
-
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
|
-
)
|
342
|
-
|
343
|
-
|
344
|
-
def display_application_logs(context: ExecutionContext, event: events.Finished) -> None:
|
345
|
-
"""Print logs captured during the application run."""
|
346
|
-
if not event.has_logs:
|
347
|
-
return
|
348
|
-
display_section_name("APPLICATION LOGS")
|
349
|
-
for result in context.results:
|
350
|
-
if not result.has_logs:
|
351
|
-
continue
|
352
|
-
display_single_log(result)
|
353
|
-
|
354
|
-
|
355
|
-
def display_single_log(result: SerializedTestResult) -> None:
|
356
|
-
display_subsection(result, None)
|
357
|
-
click.echo("\n\n".join(result.logs))
|
358
|
-
|
359
|
-
|
360
|
-
def display_statistic(context: ExecutionContext, event: events.Finished) -> None:
|
361
|
-
"""Format and print statistic collected by :obj:`models.TestResult`."""
|
362
|
-
display_section_name("SUMMARY")
|
363
|
-
click.echo()
|
364
|
-
total = event.total
|
365
|
-
if event.is_empty or not total:
|
366
|
-
click.secho("No checks were performed.", bold=True)
|
367
|
-
|
368
|
-
if total:
|
369
|
-
display_checks_statistics(total)
|
370
|
-
|
371
|
-
if context.cassette_path:
|
372
|
-
click.echo()
|
373
|
-
category = click.style("Network log", bold=True)
|
374
|
-
click.secho(f"{category}: {context.cassette_path}")
|
375
|
-
|
376
|
-
if context.junit_xml_file:
|
377
|
-
click.echo()
|
378
|
-
category = click.style("JUnit XML file", bold=True)
|
379
|
-
click.secho(f"{category}: {context.junit_xml_file}")
|
380
|
-
|
381
|
-
if event.warnings:
|
382
|
-
click.echo()
|
383
|
-
if len(event.warnings) == 1:
|
384
|
-
title = click.style("WARNING:", bold=True, fg="yellow")
|
385
|
-
warning = click.style(event.warnings[0], fg="yellow")
|
386
|
-
click.secho(f"{title} {warning}")
|
387
|
-
else:
|
388
|
-
click.secho("WARNINGS:", bold=True, fg="yellow")
|
389
|
-
for warning in event.warnings:
|
390
|
-
click.secho(f" - {warning}", fg="yellow")
|
391
|
-
|
392
|
-
if len(GLOBAL_EXPERIMENTS.enabled) > 0:
|
393
|
-
click.secho("\nExperimental Features:", bold=True)
|
394
|
-
for experiment in sorted(GLOBAL_EXPERIMENTS.enabled, key=lambda e: e.name):
|
395
|
-
click.secho(f" - {experiment.verbose_name}: {experiment.description}")
|
396
|
-
click.secho(f" Feedback: {experiment.discussion_url}")
|
397
|
-
click.echo()
|
398
|
-
click.echo(
|
399
|
-
"Your feedback is crucial for experimental features. "
|
400
|
-
"Please visit the provided URL(s) to share your thoughts."
|
401
|
-
)
|
402
|
-
|
403
|
-
if event.failed_count > 0:
|
404
|
-
click.echo(
|
405
|
-
f"\n{bold('Note')}: Use the '{SCHEMATHESIS_TEST_CASE_HEADER}' header to correlate test case ids "
|
406
|
-
"from failure messages with server logs for debugging."
|
407
|
-
)
|
408
|
-
if context.seed is not None:
|
409
|
-
seed_option = f"`--hypothesis-seed={context.seed}`"
|
410
|
-
click.secho(f"\n{bold('Note')}: To replicate these test failures, rerun with {bold(seed_option)}")
|
411
|
-
|
412
|
-
if context.report is not None and not context.is_interrupted:
|
413
|
-
if isinstance(context.report, FileReportContext):
|
414
|
-
click.echo()
|
415
|
-
display_report_metadata(context.report.queue.get())
|
416
|
-
click.secho(f"Report is saved to {context.report.filename}", bold=True)
|
417
|
-
elif isinstance(context.report, ServiceReportContext):
|
418
|
-
click.echo()
|
419
|
-
handle_service_integration(context.report)
|
420
|
-
else:
|
421
|
-
env_var = os.getenv(REPORT_SUGGESTION_ENV_VAR)
|
422
|
-
if env_var is not None and env_var.lower() in FALSE_VALUES:
|
423
|
-
return
|
424
|
-
click.echo(
|
425
|
-
f"\n{bold('Tip')}: Use the {bold('`--report`')} CLI option to visualize test results via Schemathesis.io.\n"
|
426
|
-
"We run additional conformance checks on reports from public repos."
|
427
|
-
)
|
428
|
-
if service.ci.detect() == service.ci.CIProvider.GITHUB:
|
429
|
-
click.echo(
|
430
|
-
"Optionally, for reporting results as PR comments, install the Schemathesis GitHub App:\n\n"
|
431
|
-
f" {GITHUB_APP_LINK}"
|
432
|
-
)
|
433
|
-
|
434
|
-
|
435
|
-
def handle_service_integration(context: ServiceReportContext) -> None:
|
436
|
-
"""If Schemathesis.io integration is enabled, wait for the handler & print the resulting status."""
|
437
|
-
event = context.queue.get()
|
438
|
-
title = click.style("Upload", bold=True)
|
439
|
-
if isinstance(event, service.Metadata):
|
440
|
-
display_report_metadata(event)
|
441
|
-
click.secho(f"Uploading reports to {context.service_base_url} ...", bold=True)
|
442
|
-
event = wait_for_report_handler(context.queue, title)
|
443
|
-
color = {
|
444
|
-
service.Completed: "green",
|
445
|
-
service.Error: "red",
|
446
|
-
service.Failed: "red",
|
447
|
-
service.Timeout: "red",
|
448
|
-
}[event.__class__]
|
449
|
-
status = click.style(event.status, fg=color, bold=True)
|
450
|
-
click.echo(f"{title}: {status}\r", nl=False)
|
451
|
-
click.echo()
|
452
|
-
if isinstance(event, service.Error):
|
453
|
-
click.echo()
|
454
|
-
display_service_error(event)
|
455
|
-
if isinstance(event, service.Failed):
|
456
|
-
click.echo()
|
457
|
-
click.echo(event.detail)
|
458
|
-
if isinstance(event, service.Completed):
|
459
|
-
click.echo()
|
460
|
-
click.echo(event.message)
|
461
|
-
click.echo()
|
462
|
-
click.echo(event.next_url)
|
463
|
-
|
464
|
-
|
465
|
-
def display_report_metadata(meta: service.Metadata) -> None:
|
466
|
-
if meta.ci_environment is not None:
|
467
|
-
click.secho(f"{meta.ci_environment.verbose_name} detected:", bold=True)
|
468
|
-
for key, value in meta.ci_environment.as_env().items():
|
469
|
-
if value is not None:
|
470
|
-
click.secho(f" -> {key}: {value}")
|
471
|
-
click.echo()
|
472
|
-
click.secho(f"Compressed report size: {meta.size / 1024.:,.0f} KB", bold=True)
|
473
|
-
|
474
|
-
|
475
|
-
def display_service_unauthorized(hostname: str) -> None:
|
476
|
-
click.secho("\nTo authenticate:")
|
477
|
-
click.secho(f"1. Retrieve your token from {bold(hostname)}")
|
478
|
-
click.secho(f"2. Execute {bold('`st auth login <TOKEN>`')}")
|
479
|
-
env_var = bold(f"`{service.TOKEN_ENV_VAR}`")
|
480
|
-
click.secho(
|
481
|
-
f"\nAs an alternative, supply the token directly "
|
482
|
-
f"using the {bold('`--schemathesis-io-token`')} option "
|
483
|
-
f"or the {env_var} environment variable."
|
484
|
-
)
|
485
|
-
click.echo("\nFor more information, please visit: https://schemathesis.readthedocs.io/en/stable/service.html")
|
486
|
-
|
487
|
-
|
488
|
-
def display_service_error(event: service.Error, message_prefix: str = "") -> None:
|
489
|
-
"""Show information about an error during communication with Schemathesis.io."""
|
490
|
-
from requests import RequestException, HTTPError, Response
|
491
|
-
|
492
|
-
if isinstance(event.exception, HTTPError):
|
493
|
-
response = cast(Response, event.exception.response)
|
494
|
-
status_code = response.status_code
|
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
|
-
)
|
521
|
-
elif isinstance(event.exception, RequestException):
|
522
|
-
ask_to_report(event, report_to_issues=False)
|
523
|
-
else:
|
524
|
-
ask_to_report(event)
|
525
|
-
|
526
|
-
|
527
|
-
SERVICE_ERROR_MESSAGE = "An error happened during uploading reports to Schemathesis.io"
|
528
|
-
|
529
|
-
|
530
|
-
def ask_to_report(event: service.Error, report_to_issues: bool = True, extra: str = "") -> None:
|
531
|
-
from requests import RequestException
|
532
|
-
|
533
|
-
# Likely an internal Schemathesis error
|
534
|
-
traceback = event.get_message(True)
|
535
|
-
if isinstance(event.exception, RequestException) and event.exception.response is not None:
|
536
|
-
response = f"Response: {event.exception.response.text}\n"
|
537
|
-
else:
|
538
|
-
response = ""
|
539
|
-
if report_to_issues:
|
540
|
-
ask = f"Please, consider reporting the following details to our issue tracker:\n\n {ISSUE_TRACKER_URL}\n\n"
|
541
|
-
else:
|
542
|
-
ask = ""
|
543
|
-
click.secho(
|
544
|
-
f"{SERVICE_ERROR_MESSAGE}:\n{extra}{ask}{response}\n{traceback.strip()}",
|
545
|
-
fg="red",
|
546
|
-
)
|
547
|
-
|
548
|
-
|
549
|
-
def wait_for_report_handler(queue: Queue, title: str, timeout: float = service.WORKER_FINISH_TIMEOUT) -> service.Event:
|
550
|
-
"""Wait for the Schemathesis.io handler to finish its job."""
|
551
|
-
start = time.monotonic()
|
552
|
-
spinner = create_spinner(SPINNER_REPETITION_NUMBER)
|
553
|
-
# The testing process is done, and we need to wait for the Schemathesis.io handler to finish
|
554
|
-
# It might still have some data to send
|
555
|
-
while queue.empty():
|
556
|
-
if time.monotonic() - start >= timeout:
|
557
|
-
return service.Timeout()
|
558
|
-
click.echo(f"{title}: {next(spinner)}\r", nl=False)
|
559
|
-
time.sleep(service.WORKER_CHECK_PERIOD)
|
560
|
-
return queue.get()
|
561
|
-
|
562
|
-
|
563
|
-
def create_spinner(repetitions: int) -> Generator[str, None, None]:
|
564
|
-
"""A simple spinner that yields its individual characters."""
|
565
|
-
assert repetitions > 0, "The number of repetitions should be greater than zero"
|
566
|
-
while True:
|
567
|
-
for ch in "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏":
|
568
|
-
# Skip branch coverage, as it is not possible because of the assertion above
|
569
|
-
for _ in range(repetitions): # pragma: no branch
|
570
|
-
yield ch
|
571
|
-
|
572
|
-
|
573
|
-
def display_checks_statistics(total: dict[str, dict[str | Status, int]]) -> None:
|
574
|
-
padding = 20
|
575
|
-
col1_len = max(map(len, total.keys())) + padding
|
576
|
-
col2_len = len(str(max(total.values(), key=lambda v: v["total"])["total"])) * 2 + padding
|
577
|
-
col3_len = padding
|
578
|
-
click.secho("Performed checks:", bold=True)
|
579
|
-
template = f" {{:{col1_len}}}{{:{col2_len}}}{{:{col3_len}}}"
|
580
|
-
for check_name, results in total.items():
|
581
|
-
display_check_result(check_name, results, template)
|
582
|
-
|
583
|
-
|
584
|
-
def display_check_result(check_name: str, results: dict[str | Status, int], template: str) -> None:
|
585
|
-
"""Show results of single check execution."""
|
586
|
-
if Status.failure in results:
|
587
|
-
verdict = "FAILED"
|
588
|
-
color = "red"
|
589
|
-
else:
|
590
|
-
verdict = "PASSED"
|
591
|
-
color = "green"
|
592
|
-
success = results.get(Status.success, 0)
|
593
|
-
total = results.get("total", 0)
|
594
|
-
click.echo(template.format(check_name, f"{success} / {total} passed", click.style(verdict, fg=color, bold=True)))
|
595
|
-
|
596
|
-
|
597
|
-
VERIFY_URL_SUGGESTION = "Verify that the URL points directly to the Open API schema"
|
598
|
-
|
599
|
-
|
600
|
-
SCHEMA_ERROR_SUGGESTIONS = {
|
601
|
-
# SSL-specific connection issue
|
602
|
-
SchemaErrorType.CONNECTION_SSL: DISABLE_SSL_SUGGESTION,
|
603
|
-
# Other connection problems
|
604
|
-
SchemaErrorType.CONNECTION_OTHER: f"Use {bold('`--wait-for-schema=NUM`')} to wait up to NUM seconds for schema availability.",
|
605
|
-
# Response issues
|
606
|
-
SchemaErrorType.UNEXPECTED_CONTENT_TYPE: VERIFY_URL_SUGGESTION,
|
607
|
-
SchemaErrorType.HTTP_FORBIDDEN: "Verify your API keys or authentication headers.",
|
608
|
-
SchemaErrorType.HTTP_NOT_FOUND: VERIFY_URL_SUGGESTION,
|
609
|
-
# OpenAPI specification issues
|
610
|
-
SchemaErrorType.OPEN_API_UNSPECIFIED_VERSION: f"Include the version in the schema or manually set it with {bold('`--force-schema-version`')}.",
|
611
|
-
SchemaErrorType.OPEN_API_UNSUPPORTED_VERSION: f"Proceed with {bold('`--force-schema-version`')}. Caution: May not be fully supported.",
|
612
|
-
SchemaErrorType.OPEN_API_EXPERIMENTAL_VERSION: f"Proceed with {bold('`--experimental=openapi-3.1`. Caution: May not be fully supported.')}",
|
613
|
-
SchemaErrorType.OPEN_API_INVALID_SCHEMA: DISABLE_SCHEMA_VALIDATION_SUGGESTION,
|
614
|
-
# YAML specific issues
|
615
|
-
SchemaErrorType.YAML_NUMERIC_STATUS_CODES: "Convert numeric status codes to strings.",
|
616
|
-
SchemaErrorType.YAML_NON_STRING_KEYS: "Convert non-string keys to strings.",
|
617
|
-
# Unclassified
|
618
|
-
SchemaErrorType.UNCLASSIFIED: f"If you suspect this is a Schemathesis issue and the schema is valid, please report it and include the schema if you can:\n\n {ISSUE_TRACKER_URL}",
|
619
|
-
}
|
620
|
-
|
621
|
-
|
622
|
-
def should_skip_suggestion(context: ExecutionContext, event: events.InternalError) -> bool:
|
623
|
-
return event.subtype == SchemaErrorType.CONNECTION_OTHER and context.wait_for_schema is not None
|
624
|
-
|
625
|
-
|
626
|
-
def _split_traceback(traceback: str) -> list[str]:
|
627
|
-
return [entry for entry in traceback.splitlines() if entry]
|
628
|
-
|
629
|
-
|
630
|
-
def _display_extras(extras: list[str]) -> None:
|
631
|
-
if extras:
|
632
|
-
click.echo()
|
633
|
-
for extra in extras:
|
634
|
-
click.secho(f" {extra}")
|
635
|
-
|
636
|
-
|
637
|
-
def _maybe_display_tip(suggestion: str | None) -> None:
|
638
|
-
# Display suggestion if any
|
639
|
-
if suggestion is not None:
|
640
|
-
click.secho(f"\n{click.style('Tip:', bold=True, fg='green')} {suggestion}")
|
641
|
-
|
642
|
-
|
643
|
-
def display_internal_error(context: ExecutionContext, event: events.InternalError) -> None:
|
644
|
-
click.secho(event.title, fg="red", bold=True)
|
645
|
-
click.echo()
|
646
|
-
click.secho(event.message)
|
647
|
-
if event.type == InternalErrorType.SCHEMA:
|
648
|
-
extras = event.extras
|
649
|
-
elif context.show_trace:
|
650
|
-
extras = _split_traceback(event.exception_with_traceback)
|
651
|
-
else:
|
652
|
-
extras = [event.exception]
|
653
|
-
_display_extras(extras)
|
654
|
-
if not should_skip_suggestion(context, event):
|
655
|
-
if event.type == InternalErrorType.SCHEMA and isinstance(event.subtype, SchemaErrorType):
|
656
|
-
suggestion = SCHEMA_ERROR_SUGGESTIONS.get(event.subtype)
|
657
|
-
elif context.show_trace:
|
658
|
-
suggestion = (
|
659
|
-
f"Please consider reporting the traceback above to our issue tracker:\n\n {ISSUE_TRACKER_URL}."
|
660
|
-
)
|
661
|
-
else:
|
662
|
-
suggestion = f"To see full tracebacks, add {bold('`--show-trace`')} to your CLI options"
|
663
|
-
_maybe_display_tip(suggestion)
|
664
|
-
|
665
|
-
|
666
|
-
def handle_initialized(context: ExecutionContext, event: events.Initialized) -> None:
|
667
|
-
"""Display information about the test session."""
|
668
|
-
context.operations_count = cast(int, event.operations_count) # INVARIANT: should not be `None`
|
669
|
-
context.seed = event.seed
|
670
|
-
display_section_name("Schemathesis test session starts")
|
671
|
-
if context.verbosity > 0:
|
672
|
-
versions = (
|
673
|
-
f"platform {platform.system()} -- "
|
674
|
-
f"Python {platform.python_version()}, "
|
675
|
-
f"schemathesis-{SCHEMATHESIS_VERSION}, "
|
676
|
-
f"hypothesis-{metadata.version('hypothesis')}, "
|
677
|
-
f"hypothesis_jsonschema-{metadata.version('hypothesis_jsonschema')}, "
|
678
|
-
f"jsonschema-{metadata.version('jsonschema')}"
|
679
|
-
)
|
680
|
-
click.echo(versions)
|
681
|
-
click.echo(f"rootdir: {os.getcwd()}")
|
682
|
-
click.echo(f"Hypothesis: {context.hypothesis_settings.show_changed()}")
|
683
|
-
if event.location is not None:
|
684
|
-
click.secho(f"Schema location: {event.location}", bold=True)
|
685
|
-
click.secho(f"Base URL: {event.base_url}", bold=True)
|
686
|
-
click.secho(f"Specification version: {event.specification_name}", bold=True)
|
687
|
-
if context.seed is not None:
|
688
|
-
click.secho(f"Random seed: {context.seed}", bold=True)
|
689
|
-
click.secho(f"Workers: {context.workers_num}", bold=True)
|
690
|
-
if context.rate_limit is not None:
|
691
|
-
click.secho(f"Rate limit: {context.rate_limit}", bold=True)
|
692
|
-
click.secho(f"Collected API operations: {context.operations_count}", bold=True)
|
693
|
-
links_count = cast(int, event.links_count)
|
694
|
-
click.secho(f"Collected API links: {links_count}", bold=True)
|
695
|
-
if isinstance(context.report, ServiceReportContext):
|
696
|
-
click.secho("Report to Schemathesis.io: ENABLED", bold=True)
|
697
|
-
if context.operations_count >= 1:
|
698
|
-
click.echo()
|
699
|
-
|
700
|
-
|
701
|
-
TRUNCATION_PLACEHOLDER = "[...]"
|
702
|
-
|
703
|
-
|
704
|
-
def handle_before_execution(context: ExecutionContext, event: events.BeforeExecution) -> None:
|
705
|
-
"""Display what method / path will be tested next."""
|
706
|
-
# We should display execution result + percentage in the end. For example:
|
707
|
-
max_length = get_terminal_width() - len(" . [XXX%]") - len(TRUNCATION_PLACEHOLDER)
|
708
|
-
message = event.verbose_name
|
709
|
-
if event.recursion_level > 0:
|
710
|
-
message = f"{' ' * event.recursion_level}-> {message}"
|
711
|
-
# This value is not `None` - the value is set in runtime before this line
|
712
|
-
context.operations_count += 1 # type: ignore
|
713
|
-
|
714
|
-
message = message[:max_length] + (message[max_length:] and "[...]") + " "
|
715
|
-
context.current_line_length = len(message)
|
716
|
-
click.echo(message, nl=False)
|
717
|
-
|
718
|
-
|
719
|
-
def handle_after_execution(context: ExecutionContext, event: events.AfterExecution) -> None:
|
720
|
-
"""Display the execution result + current progress at the same line with the method / path names."""
|
721
|
-
context.operations_processed += 1
|
722
|
-
context.results.append(event.result)
|
723
|
-
display_execution_result(context, event)
|
724
|
-
display_percentage(context, event)
|
725
|
-
|
726
|
-
|
727
|
-
def handle_finished(context: ExecutionContext, event: events.Finished) -> None:
|
728
|
-
"""Show the outcome of the whole testing session."""
|
729
|
-
click.echo()
|
730
|
-
display_hypothesis_output(context.hypothesis_output)
|
731
|
-
display_errors(context, event)
|
732
|
-
display_failures(context, event)
|
733
|
-
display_application_logs(context, event)
|
734
|
-
display_statistic(context, event)
|
735
|
-
click.echo()
|
736
|
-
display_summary(event)
|
737
|
-
|
738
|
-
|
739
|
-
def handle_interrupted(context: ExecutionContext, event: events.Interrupted) -> None:
|
740
|
-
click.echo()
|
741
|
-
context.is_interrupted = True
|
742
|
-
display_section_name("KeyboardInterrupt", "!", bold=False)
|
743
|
-
|
744
|
-
|
745
|
-
def handle_internal_error(context: ExecutionContext, event: events.InternalError) -> None:
|
746
|
-
display_internal_error(context, event)
|
747
|
-
raise click.Abort
|
748
|
-
|
749
|
-
|
750
|
-
class DefaultOutputStyleHandler(EventHandler):
|
751
|
-
def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
|
752
|
-
"""Choose and execute a proper handler for the given event."""
|
753
|
-
if isinstance(event, events.Initialized):
|
754
|
-
handle_initialized(context, event)
|
755
|
-
if isinstance(event, events.BeforeExecution):
|
756
|
-
handle_before_execution(context, event)
|
757
|
-
if isinstance(event, events.AfterExecution):
|
758
|
-
context.hypothesis_output.extend(event.hypothesis_output)
|
759
|
-
handle_after_execution(context, event)
|
760
|
-
if isinstance(event, events.Finished):
|
761
|
-
handle_finished(context, event)
|
762
|
-
if isinstance(event, events.Interrupted):
|
763
|
-
handle_interrupted(context, event)
|
764
|
-
if isinstance(event, events.InternalError):
|
765
|
-
handle_internal_error(context, event)
|