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.
Files changed (146) hide show
  1. schemathesis/__init__.py +6 -6
  2. schemathesis/_compat.py +2 -2
  3. schemathesis/_dependency_versions.py +4 -2
  4. schemathesis/_hypothesis.py +369 -56
  5. schemathesis/_lazy_import.py +1 -0
  6. schemathesis/_override.py +5 -4
  7. schemathesis/_patches.py +21 -0
  8. schemathesis/_rate_limiter.py +7 -0
  9. schemathesis/_xml.py +75 -22
  10. schemathesis/auths.py +78 -16
  11. schemathesis/checks.py +21 -9
  12. schemathesis/cli/__init__.py +783 -432
  13. schemathesis/cli/__main__.py +4 -0
  14. schemathesis/cli/callbacks.py +58 -13
  15. schemathesis/cli/cassettes.py +233 -47
  16. schemathesis/cli/constants.py +8 -2
  17. schemathesis/cli/context.py +22 -5
  18. schemathesis/cli/debug.py +2 -1
  19. schemathesis/cli/handlers.py +4 -1
  20. schemathesis/cli/junitxml.py +103 -22
  21. schemathesis/cli/options.py +15 -4
  22. schemathesis/cli/output/default.py +258 -112
  23. schemathesis/cli/output/short.py +23 -8
  24. schemathesis/cli/reporting.py +79 -0
  25. schemathesis/cli/sanitization.py +6 -0
  26. schemathesis/code_samples.py +5 -3
  27. schemathesis/constants.py +1 -0
  28. schemathesis/contrib/openapi/__init__.py +1 -1
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
  30. schemathesis/contrib/openapi/formats/uuid.py +2 -1
  31. schemathesis/contrib/unique_data.py +3 -3
  32. schemathesis/exceptions.py +76 -65
  33. schemathesis/experimental/__init__.py +35 -0
  34. schemathesis/extra/_aiohttp.py +1 -0
  35. schemathesis/extra/_flask.py +4 -1
  36. schemathesis/extra/_server.py +1 -0
  37. schemathesis/extra/pytest_plugin.py +17 -25
  38. schemathesis/failures.py +77 -9
  39. schemathesis/filters.py +185 -8
  40. schemathesis/fixups/__init__.py +1 -0
  41. schemathesis/fixups/fast_api.py +2 -2
  42. schemathesis/fixups/utf8_bom.py +1 -2
  43. schemathesis/generation/__init__.py +20 -36
  44. schemathesis/generation/_hypothesis.py +59 -0
  45. schemathesis/generation/_methods.py +44 -0
  46. schemathesis/generation/coverage.py +931 -0
  47. schemathesis/graphql.py +0 -1
  48. schemathesis/hooks.py +89 -12
  49. schemathesis/internal/checks.py +84 -0
  50. schemathesis/internal/copy.py +22 -3
  51. schemathesis/internal/deprecation.py +6 -2
  52. schemathesis/internal/diff.py +15 -0
  53. schemathesis/internal/extensions.py +27 -0
  54. schemathesis/internal/jsonschema.py +2 -1
  55. schemathesis/internal/output.py +68 -0
  56. schemathesis/internal/result.py +1 -1
  57. schemathesis/internal/transformation.py +11 -0
  58. schemathesis/lazy.py +138 -25
  59. schemathesis/loaders.py +7 -5
  60. schemathesis/models.py +318 -211
  61. schemathesis/parameters.py +4 -0
  62. schemathesis/runner/__init__.py +50 -15
  63. schemathesis/runner/events.py +65 -5
  64. schemathesis/runner/impl/context.py +104 -0
  65. schemathesis/runner/impl/core.py +388 -177
  66. schemathesis/runner/impl/solo.py +19 -29
  67. schemathesis/runner/impl/threadpool.py +70 -79
  68. schemathesis/runner/probes.py +11 -9
  69. schemathesis/runner/serialization.py +150 -17
  70. schemathesis/sanitization.py +5 -1
  71. schemathesis/schemas.py +170 -102
  72. schemathesis/serializers.py +7 -2
  73. schemathesis/service/ci.py +1 -0
  74. schemathesis/service/client.py +39 -6
  75. schemathesis/service/events.py +5 -1
  76. schemathesis/service/extensions.py +224 -0
  77. schemathesis/service/hosts.py +6 -2
  78. schemathesis/service/metadata.py +25 -0
  79. schemathesis/service/models.py +211 -2
  80. schemathesis/service/report.py +6 -6
  81. schemathesis/service/serialization.py +45 -71
  82. schemathesis/service/usage.py +1 -0
  83. schemathesis/specs/graphql/_cache.py +26 -0
  84. schemathesis/specs/graphql/loaders.py +25 -5
  85. schemathesis/specs/graphql/nodes.py +1 -0
  86. schemathesis/specs/graphql/scalars.py +2 -2
  87. schemathesis/specs/graphql/schemas.py +130 -100
  88. schemathesis/specs/graphql/validation.py +1 -2
  89. schemathesis/specs/openapi/__init__.py +1 -0
  90. schemathesis/specs/openapi/_cache.py +123 -0
  91. schemathesis/specs/openapi/_hypothesis.py +78 -60
  92. schemathesis/specs/openapi/checks.py +504 -25
  93. schemathesis/specs/openapi/converter.py +31 -4
  94. schemathesis/specs/openapi/definitions.py +10 -17
  95. schemathesis/specs/openapi/examples.py +126 -12
  96. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  97. schemathesis/specs/openapi/expressions/context.py +1 -1
  98. schemathesis/specs/openapi/expressions/extractors.py +26 -0
  99. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  100. schemathesis/specs/openapi/expressions/nodes.py +29 -6
  101. schemathesis/specs/openapi/expressions/parser.py +26 -5
  102. schemathesis/specs/openapi/formats.py +44 -0
  103. schemathesis/specs/openapi/links.py +125 -42
  104. schemathesis/specs/openapi/loaders.py +77 -36
  105. schemathesis/specs/openapi/media_types.py +34 -0
  106. schemathesis/specs/openapi/negative/__init__.py +6 -3
  107. schemathesis/specs/openapi/negative/mutations.py +21 -6
  108. schemathesis/specs/openapi/parameters.py +39 -25
  109. schemathesis/specs/openapi/patterns.py +137 -0
  110. schemathesis/specs/openapi/references.py +37 -7
  111. schemathesis/specs/openapi/schemas.py +360 -241
  112. schemathesis/specs/openapi/security.py +25 -7
  113. schemathesis/specs/openapi/serialization.py +1 -0
  114. schemathesis/specs/openapi/stateful/__init__.py +198 -70
  115. schemathesis/specs/openapi/stateful/statistic.py +198 -0
  116. schemathesis/specs/openapi/stateful/types.py +14 -0
  117. schemathesis/specs/openapi/utils.py +6 -1
  118. schemathesis/specs/openapi/validation.py +1 -0
  119. schemathesis/stateful/__init__.py +35 -21
  120. schemathesis/stateful/config.py +97 -0
  121. schemathesis/stateful/context.py +135 -0
  122. schemathesis/stateful/events.py +274 -0
  123. schemathesis/stateful/runner.py +309 -0
  124. schemathesis/stateful/sink.py +68 -0
  125. schemathesis/stateful/state_machine.py +67 -38
  126. schemathesis/stateful/statistic.py +22 -0
  127. schemathesis/stateful/validation.py +100 -0
  128. schemathesis/targets.py +33 -1
  129. schemathesis/throttling.py +25 -5
  130. schemathesis/transports/__init__.py +354 -0
  131. schemathesis/transports/asgi.py +7 -0
  132. schemathesis/transports/auth.py +25 -2
  133. schemathesis/transports/content_types.py +3 -1
  134. schemathesis/transports/headers.py +2 -1
  135. schemathesis/transports/responses.py +9 -4
  136. schemathesis/types.py +9 -0
  137. schemathesis/utils.py +11 -16
  138. schemathesis-3.39.7.dist-info/METADATA +293 -0
  139. schemathesis-3.39.7.dist-info/RECORD +160 -0
  140. {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
  141. schemathesis/specs/openapi/filters.py +0 -49
  142. schemathesis/specs/openapi/stateful/links.py +0 -92
  143. schemathesis-3.25.6.dist-info/METADATA +0 -356
  144. schemathesis-3.25.6.dist-info/RECORD +0 -134
  145. {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
  146. {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 itertools import groupby
11
- from queue import Queue
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 RuntimeErrorType, prepare_response_payload
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 SerializedCheck, SerializedError, SerializedTestResult, deduplicate_failures
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, event: events.AfterExecution) -> None:
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
- Status.success: (".", "green"),
67
- Status.failure: ("F", "red"),
68
- Status.error: ("E", "red"),
69
- Status.skip: ("S", "yellow"),
70
- }[event.status]
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
- if not event.has_errors:
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 not error.exception.startswith(
185
- (
186
- "DeadlineExceeded",
187
- "OperationSchemaError",
188
- "requests.exceptions",
189
- "SerializationNotPossible",
190
- "hypothesis.errors.FailedHealthCheck",
191
- "hypothesis.errors.InvalidArgument: Scalar ",
192
- "hypothesis.errors.InvalidArgument: min_size=",
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 = _split_traceback(error.exception_with_traceback)
271
+ extras = split_traceback(error.exception_with_traceback)
247
272
  else:
248
273
  extras = []
249
274
  _display_extras(extras)
250
- suggestion = RUNTIME_ERROR_SUGGESTIONS.get(error.type)
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
- TEST_CASE_ID_TITLE = "Test Case ID"
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
- if check.context is not None:
288
- title = check.context.title
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
- message = textwrap.indent(message, prefix=" ")
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
- response_body = check.response.body
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
- payload = base64.b64decode(response_body).decode(encoding)
315
- payload = prepare_response_payload(payload)
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.:,.0f} KB", bold=True)
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
- status_code = response.status_code
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 = _split_traceback(event.exception_with_traceback)
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}\r", bold=True, nl=False)
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
- if isinstance(event, events.BeforeProbing):
914
+ elif isinstance(event, events.BeforeProbing):
777
915
  handle_before_probing(context, event)
778
- if isinstance(event, events.AfterProbing):
916
+ elif isinstance(event, events.AfterProbing):
779
917
  handle_after_probing(context, event)
780
- if isinstance(event, events.BeforeExecution):
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
- if isinstance(event, events.AfterExecution):
924
+ elif isinstance(event, events.AfterExecution):
783
925
  context.hypothesis_output.extend(event.hypothesis_output)
784
926
  handle_after_execution(context, event)
785
- if isinstance(event, events.Finished):
927
+ elif isinstance(event, events.Finished):
786
928
  handle_finished(context, event)
787
- if isinstance(event, events.Interrupted):
929
+ elif isinstance(event, events.Interrupted):
788
930
  handle_interrupted(context, event)
789
- if isinstance(event, events.InternalError):
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)
@@ -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
- if isinstance(event, events.BeforeProbing):
36
+ elif isinstance(event, events.BeforeProbing):
30
37
  default.handle_before_probing(context, event)
31
- if isinstance(event, events.AfterProbing):
38
+ elif isinstance(event, events.AfterProbing):
32
39
  default.handle_after_probing(context, event)
33
- if isinstance(event, events.BeforeExecution):
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
- if isinstance(event, events.AfterExecution):
46
+ elif isinstance(event, events.AfterExecution):
36
47
  handle_after_execution(context, event)
37
- if isinstance(event, events.Finished):
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
- if isinstance(event, events.Interrupted):
52
+ elif isinstance(event, events.Interrupted):
42
53
  default.handle_interrupted(context, event)
43
- if isinstance(event, events.InternalError):
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)