schemathesis 4.0.0a10__py3-none-any.whl → 4.0.0a11__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 (92) hide show
  1. schemathesis/__init__.py +3 -7
  2. schemathesis/checks.py +17 -7
  3. schemathesis/cli/commands/__init__.py +51 -3
  4. schemathesis/cli/commands/data.py +10 -0
  5. schemathesis/cli/commands/run/__init__.py +147 -260
  6. schemathesis/cli/commands/run/context.py +2 -3
  7. schemathesis/cli/commands/run/events.py +4 -0
  8. schemathesis/cli/commands/run/executor.py +60 -73
  9. schemathesis/cli/commands/run/filters.py +15 -165
  10. schemathesis/cli/commands/run/handlers/cassettes.py +105 -104
  11. schemathesis/cli/commands/run/handlers/junitxml.py +5 -4
  12. schemathesis/cli/commands/run/handlers/output.py +26 -47
  13. schemathesis/cli/commands/run/loaders.py +35 -50
  14. schemathesis/cli/commands/run/validation.py +36 -161
  15. schemathesis/cli/core.py +5 -3
  16. schemathesis/cli/ext/fs.py +7 -5
  17. schemathesis/cli/ext/options.py +0 -21
  18. schemathesis/config/__init__.py +188 -0
  19. schemathesis/config/_auth.py +51 -0
  20. schemathesis/config/_checks.py +268 -0
  21. schemathesis/config/_diff_base.py +99 -0
  22. schemathesis/config/_env.py +21 -0
  23. schemathesis/config/_error.py +156 -0
  24. schemathesis/config/_generation.py +150 -0
  25. schemathesis/config/_health_check.py +24 -0
  26. schemathesis/config/_operations.py +313 -0
  27. schemathesis/config/_output.py +171 -0
  28. schemathesis/config/_parameters.py +19 -0
  29. schemathesis/config/_phases.py +151 -0
  30. schemathesis/config/_projects.py +495 -0
  31. schemathesis/config/_rate_limit.py +17 -0
  32. schemathesis/config/_report.py +116 -0
  33. schemathesis/config/_validator.py +9 -0
  34. schemathesis/config/schema.json +837 -0
  35. schemathesis/core/__init__.py +2 -0
  36. schemathesis/core/compat.py +16 -9
  37. schemathesis/core/errors.py +19 -2
  38. schemathesis/core/failures.py +6 -7
  39. schemathesis/core/hooks.py +20 -0
  40. schemathesis/core/output/__init__.py +14 -37
  41. schemathesis/core/output/sanitization.py +3 -146
  42. schemathesis/core/validation.py +16 -0
  43. schemathesis/engine/__init__.py +2 -4
  44. schemathesis/engine/context.py +41 -43
  45. schemathesis/engine/core.py +7 -5
  46. schemathesis/engine/phases/__init__.py +10 -0
  47. schemathesis/engine/phases/probes.py +8 -8
  48. schemathesis/engine/phases/stateful/_executor.py +68 -43
  49. schemathesis/engine/phases/unit/__init__.py +23 -15
  50. schemathesis/engine/phases/unit/_executor.py +77 -17
  51. schemathesis/engine/phases/unit/_pool.py +1 -1
  52. schemathesis/errors.py +2 -0
  53. schemathesis/filters.py +2 -3
  54. schemathesis/generation/__init__.py +6 -31
  55. schemathesis/generation/case.py +5 -3
  56. schemathesis/generation/coverage.py +153 -123
  57. schemathesis/generation/hypothesis/builder.py +40 -14
  58. schemathesis/generation/meta.py +3 -3
  59. schemathesis/generation/overrides.py +37 -1
  60. schemathesis/generation/stateful/state_machine.py +8 -1
  61. schemathesis/graphql/loaders.py +21 -12
  62. schemathesis/openapi/checks.py +12 -8
  63. schemathesis/openapi/generation/filters.py +10 -8
  64. schemathesis/openapi/loaders.py +22 -13
  65. schemathesis/pytest/lazy.py +2 -5
  66. schemathesis/pytest/plugin.py +11 -2
  67. schemathesis/schemas.py +13 -61
  68. schemathesis/specs/graphql/schemas.py +11 -15
  69. schemathesis/specs/openapi/_hypothesis.py +12 -8
  70. schemathesis/specs/openapi/checks.py +16 -18
  71. schemathesis/specs/openapi/examples.py +4 -3
  72. schemathesis/specs/openapi/formats.py +2 -2
  73. schemathesis/specs/openapi/negative/__init__.py +2 -2
  74. schemathesis/specs/openapi/patterns.py +46 -16
  75. schemathesis/specs/openapi/references.py +2 -3
  76. schemathesis/specs/openapi/schemas.py +11 -20
  77. schemathesis/specs/openapi/stateful/__init__.py +10 -5
  78. schemathesis/transport/prepare.py +7 -6
  79. schemathesis/transport/requests.py +3 -1
  80. schemathesis/transport/wsgi.py +3 -4
  81. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a11.dist-info}/METADATA +7 -8
  82. schemathesis-4.0.0a11.dist-info/RECORD +166 -0
  83. schemathesis/cli/commands/run/checks.py +0 -79
  84. schemathesis/cli/commands/run/hypothesis.py +0 -78
  85. schemathesis/cli/commands/run/reports.py +0 -72
  86. schemathesis/cli/hooks.py +0 -36
  87. schemathesis/engine/config.py +0 -59
  88. schemathesis/experimental/__init__.py +0 -72
  89. schemathesis-4.0.0a10.dist-info/RECORD +0 -153
  90. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a11.dist-info}/WHEEL +0 -0
  91. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a11.dist-info}/entry_points.txt +0 -0
  92. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a11.dist-info}/licenses/LICENSE +0 -0
@@ -6,16 +6,16 @@ import sys
6
6
  import threading
7
7
  from dataclasses import dataclass, field
8
8
  from http.cookies import SimpleCookie
9
+ from pathlib import Path
9
10
  from queue import Queue
10
11
  from typing import IO, Callable, Iterator
11
12
  from urllib.parse import parse_qsl, urlparse
12
13
 
13
14
  import harfile
14
- from click.utils import LazyFile
15
15
 
16
16
  from schemathesis.cli.commands.run.context import ExecutionContext
17
17
  from schemathesis.cli.commands.run.handlers.base import EventHandler
18
- from schemathesis.cli.commands.run.reports import ReportFormat
18
+ from schemathesis.config import ProjectConfig, ReportFormat, SchemathesisConfig
19
19
  from schemathesis.core.output.sanitization import sanitize_url, sanitize_value
20
20
  from schemathesis.core.transforms import deepclone
21
21
  from schemathesis.core.transport import Response
@@ -33,17 +33,15 @@ class CassetteWriter(EventHandler):
33
33
  """Write network interactions to a cassette."""
34
34
 
35
35
  format: ReportFormat
36
- path: LazyFile
37
- sanitize_output: bool = True
38
- preserve_bytes: bool = False
36
+ path: Path
37
+ config: ProjectConfig
39
38
  queue: Queue = field(default_factory=Queue)
40
39
  worker: threading.Thread = field(init=False)
41
40
 
42
41
  def __post_init__(self) -> None:
43
42
  kwargs = {
44
43
  "path": self.path,
45
- "sanitize_output": self.sanitize_output,
46
- "preserve_bytes": self.preserve_bytes,
44
+ "config": self.config,
47
45
  "queue": self.queue,
48
46
  }
49
47
  writer: Callable
@@ -55,7 +53,7 @@ class CassetteWriter(EventHandler):
55
53
  self.worker.start()
56
54
 
57
55
  def start(self, ctx: ExecutionContext) -> None:
58
- self.queue.put(Initialize(seed=ctx.seed))
56
+ self.queue.put(Initialize(seed=ctx.config.seed))
59
57
 
60
58
  def handle_event(self, ctx: ExecutionContext, event: events.EngineEvent) -> None:
61
59
  if isinstance(event, events.ScenarioFinished):
@@ -103,7 +101,7 @@ def get_command_representation() -> str:
103
101
  return f"st {args}"
104
102
 
105
103
 
106
- def vcr_writer(path: LazyFile, sanitize_output: bool, preserve_bytes: bool, queue: Queue) -> None:
104
+ def vcr_writer(path: Path, config: ProjectConfig, queue: Queue) -> None:
107
105
  """Write YAML to a file in an incremental manner.
108
106
 
109
107
  This implementation doesn't use `pyyaml` package and composes YAML manually as string due to the following reasons:
@@ -114,16 +112,15 @@ def vcr_writer(path: LazyFile, sanitize_output: bool, preserve_bytes: bool, queu
114
112
  providing tags, anchors to have incremental writing, with primitive types it is much simpler.
115
113
  """
116
114
  current_id = 1
117
- stream = path.open()
118
115
 
119
116
  def format_header_values(values: list[str]) -> str:
120
117
  return "\n".join(f" - {json.dumps(v)}" for v in values)
121
118
 
122
- if sanitize_output:
119
+ if config.output.sanitization.enabled:
123
120
 
124
121
  def format_headers(headers: dict[str, list[str]]) -> str:
125
122
  headers = deepclone(headers)
126
- sanitize_value(headers)
123
+ sanitize_value(headers, config=config.output.sanitization)
127
124
  return "\n".join(f' "{name}":\n{format_header_values(values)}' for name, values in headers.items())
128
125
 
129
126
  else:
@@ -145,7 +142,7 @@ def vcr_writer(path: LazyFile, sanitize_output: bool, preserve_bytes: bool, queu
145
142
  checks:
146
143
  {items}"""
147
144
 
148
- if preserve_bytes:
145
+ if config.reports.preserve_bytes:
149
146
 
150
147
  def format_request_body(output: IO, request: Request) -> None:
151
148
  if request.encoded_body is not None:
@@ -188,102 +185,105 @@ def vcr_writer(path: LazyFile, sanitize_output: bool, preserve_bytes: bool, queu
188
185
  )
189
186
  write_double_quoted(output, string)
190
187
 
191
- while True:
192
- item = queue.get()
193
- if isinstance(item, Initialize):
194
- stream.write(
195
- f"""command: '{get_command_representation()}'
188
+ with open(path, "w", encoding="utf-8") as stream:
189
+ while True:
190
+ item = queue.get()
191
+ if isinstance(item, Initialize):
192
+ stream.write(
193
+ f"""command: '{get_command_representation()}'
196
194
  recorded_with: 'Schemathesis {SCHEMATHESIS_VERSION}'
197
195
  seed: {item.seed}
198
196
  http_interactions:"""
199
- )
200
- elif isinstance(item, Process):
201
- for case_id, interaction in item.recorder.interactions.items():
202
- case = item.recorder.cases[case_id]
203
- if interaction.response is not None:
204
- if case_id in item.recorder.checks:
205
- checks = item.recorder.checks[case_id]
206
- status = Status.SUCCESS
207
- for check in checks:
208
- if check.status == Status.FAILURE:
209
- status = check.status
210
- break
197
+ )
198
+ elif isinstance(item, Process):
199
+ for case_id, interaction in item.recorder.interactions.items():
200
+ case = item.recorder.cases[case_id]
201
+ if interaction.response is not None:
202
+ if case_id in item.recorder.checks:
203
+ checks = item.recorder.checks[case_id]
204
+ status = Status.SUCCESS
205
+ for check in checks:
206
+ if check.status == Status.FAILURE:
207
+ status = check.status
208
+ break
209
+ else:
210
+ # NOTE: Checks recording could be skipped if Schemathesis start skipping just
211
+ # discovered failures in order to get past them and potentially discover more failures
212
+ checks = []
213
+ status = Status.SKIP
211
214
  else:
212
- # NOTE: Checks recording could be skipped if Schemathesis start skipping just
213
- # discovered failures in order to get past them and potentially discover more failures
214
215
  checks = []
215
- status = Status.SKIP
216
- else:
217
- checks = []
218
- status = Status.ERROR
219
- # Body payloads are handled via separate `stream.write` calls to avoid some allocations
220
- stream.write(
221
- f"""\n- id: '{case_id}'
216
+ status = Status.ERROR
217
+ # Body payloads are handled via separate `stream.write` calls to avoid some allocations
218
+ stream.write(
219
+ f"""\n- id: '{case_id}'
222
220
  status: '{status.name}'"""
223
- )
224
- meta = case.value.meta
225
- if meta is not None:
226
- # Start metadata block
227
- stream.write(f"""
221
+ )
222
+ meta = case.value.meta
223
+ if meta is not None:
224
+ # Start metadata block
225
+ stream.write(f"""
228
226
  generation:
229
227
  time: {meta.generation.time}
230
228
  mode: {meta.generation.mode.value}
231
229
  components:""")
232
230
 
233
- # Write components
234
- for kind, info in meta.components.items():
235
- stream.write(f"""
231
+ # Write components
232
+ for kind, info in meta.components.items():
233
+ stream.write(f"""
236
234
  {kind.value}:
237
235
  mode: '{info.mode.value}'""")
238
- # Write phase info
239
- stream.write("\n phase:")
240
- stream.write(f"\n name: '{meta.phase.name.value}'")
241
- stream.write("\n data: ")
242
-
243
- # Write phase-specific data
244
- if isinstance(meta.phase.data, CoveragePhaseData):
245
- stream.write("""
236
+ # Write phase info
237
+ stream.write("\n phase:")
238
+ stream.write(f"\n name: '{meta.phase.name.value}'")
239
+ stream.write("\n data: ")
240
+
241
+ # Write phase-specific data
242
+ if isinstance(meta.phase.data, CoveragePhaseData):
243
+ stream.write("""
246
244
  description: """)
247
- write_double_quoted(stream, meta.phase.data.description)
248
- stream.write("""
245
+ write_double_quoted(stream, meta.phase.data.description)
246
+ stream.write("""
249
247
  location: """)
250
- write_double_quoted(stream, meta.phase.data.location)
251
- stream.write("""
248
+ write_double_quoted(stream, meta.phase.data.location)
249
+ stream.write("""
252
250
  parameter: """)
253
- if meta.phase.data.parameter is not None:
254
- write_double_quoted(stream, meta.phase.data.parameter)
255
- else:
256
- stream.write("null")
257
- stream.write("""
251
+ if meta.phase.data.parameter is not None:
252
+ write_double_quoted(stream, meta.phase.data.parameter)
253
+ else:
254
+ stream.write("null")
255
+ stream.write("""
258
256
  parameter_location: """)
259
- if meta.phase.data.parameter_location is not None:
260
- write_double_quoted(stream, meta.phase.data.parameter_location)
257
+ if meta.phase.data.parameter_location is not None:
258
+ write_double_quoted(stream, meta.phase.data.parameter_location)
259
+ else:
260
+ stream.write("null")
261
261
  else:
262
- stream.write("null")
262
+ # Empty objects for these phases
263
+ stream.write("{}")
263
264
  else:
264
- # Empty objects for these phases
265
- stream.write("{}")
266
- else:
267
- stream.write("null")
265
+ stream.write("null")
268
266
 
269
- if sanitize_output:
270
- uri = sanitize_url(interaction.request.uri)
271
- else:
272
- uri = interaction.request.uri
273
- recorded_at = datetime.datetime.fromtimestamp(interaction.timestamp, datetime.timezone.utc).isoformat()
274
- stream.write(
275
- f"""
267
+ if config.output.sanitization.enabled:
268
+ uri = sanitize_url(interaction.request.uri, config=config.output.sanitization)
269
+ else:
270
+ uri = interaction.request.uri
271
+ recorded_at = datetime.datetime.fromtimestamp(
272
+ interaction.timestamp, datetime.timezone.utc
273
+ ).isoformat()
274
+ stream.write(
275
+ f"""
276
276
  recorded_at: '{recorded_at}'{format_checks(checks)}
277
277
  request:
278
278
  uri: '{uri}'
279
279
  method: '{interaction.request.method}'
280
280
  headers:
281
281
  {format_headers(interaction.request.headers)}"""
282
- )
283
- format_request_body(stream, interaction.request)
284
- if interaction.response is not None:
285
- stream.write(
286
- f"""
282
+ )
283
+ format_request_body(stream, interaction.request)
284
+ if interaction.response is not None:
285
+ stream.write(
286
+ f"""
287
287
  response:
288
288
  status:
289
289
  code: '{interaction.response.status_code}'
@@ -292,19 +292,18 @@ http_interactions:"""
292
292
  headers:
293
293
  {format_headers(interaction.response.headers)}
294
294
  """
295
- )
296
- format_response_body(stream, interaction.response)
297
- stream.write(
298
- f"""
295
+ )
296
+ format_response_body(stream, interaction.response)
297
+ stream.write(
298
+ f"""
299
299
  http_version: '{interaction.response.http_version}'"""
300
- )
301
- else:
302
- stream.write("""
300
+ )
301
+ else:
302
+ stream.write("""
303
303
  response: null""")
304
- current_id += 1
305
- else:
306
- break
307
- path.close()
304
+ current_id += 1
305
+ else:
306
+ break
308
307
 
309
308
 
310
309
  def write_double_quoted(stream: IO, text: str | None) -> None:
@@ -350,14 +349,14 @@ def write_double_quoted(stream: IO, text: str | None) -> None:
350
349
  stream.write('"')
351
350
 
352
351
 
353
- def har_writer(path: LazyFile, sanitize_output: bool, preserve_bytes: bool, queue: Queue) -> None:
352
+ def har_writer(path: Path, config: SchemathesisConfig, queue: Queue) -> None:
354
353
  with harfile.open(path) as har:
355
354
  while True:
356
355
  item = queue.get()
357
356
  if isinstance(item, Process):
358
357
  for interaction in item.recorder.interactions.values():
359
- if sanitize_output:
360
- uri = sanitize_url(interaction.request.uri)
358
+ if config.output.sanitization.enabled:
359
+ uri = sanitize_url(interaction.request.uri, config=config.output.sanitization)
361
360
  else:
362
361
  uri = interaction.request.uri
363
362
  query_params = urlparse(uri).query
@@ -365,7 +364,7 @@ def har_writer(path: LazyFile, sanitize_output: bool, preserve_bytes: bool, queu
365
364
  post_data = harfile.PostData(
366
365
  mimeType=interaction.request.headers.get("Content-Type", [""])[0],
367
366
  text=interaction.request.encoded_body
368
- if preserve_bytes
367
+ if config.reports.preserve_bytes
369
368
  else interaction.request.body.decode("utf-8", "replace"),
370
369
  )
371
370
  else:
@@ -376,16 +375,18 @@ def har_writer(path: LazyFile, sanitize_output: bool, preserve_bytes: bool, queu
376
375
  size=interaction.response.body_size or 0,
377
376
  mimeType=content_type,
378
377
  text=interaction.response.encoded_body
379
- if preserve_bytes
378
+ if config.reports.preserve_bytes
380
379
  else interaction.response.content.decode("utf-8", "replace")
381
380
  if interaction.response.content is not None
382
381
  else None,
383
- encoding="base64" if interaction.response.content is not None and preserve_bytes else None,
382
+ encoding="base64"
383
+ if interaction.response.content is not None and config.reports.preserve_bytes
384
+ else None,
384
385
  )
385
386
  http_version = f"HTTP/{interaction.response.http_version}"
386
- if sanitize_output:
387
+ if config.output.sanitization.enabled:
387
388
  headers = deepclone(interaction.response.headers)
388
- sanitize_value(headers)
389
+ sanitize_value(headers, config=config.output.sanitization)
389
390
  else:
390
391
  headers = interaction.response.headers
391
392
  response = harfile.Response(
@@ -405,9 +406,9 @@ def har_writer(path: LazyFile, sanitize_output: bool, preserve_bytes: bool, queu
405
406
  time = 0
406
407
  http_version = ""
407
408
 
408
- if sanitize_output:
409
+ if config.output.sanitization.enabled:
409
410
  headers = deepclone(interaction.request.headers)
410
- sanitize_value(headers)
411
+ sanitize_value(headers, config=config.output.sanitization)
411
412
  else:
412
413
  headers = interaction.request.headers
413
414
  started_datetime = datetime.datetime.fromtimestamp(
@@ -2,9 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  import platform
4
4
  from dataclasses import dataclass, field
5
+ from pathlib import Path
5
6
  from typing import Iterable
6
7
 
7
- from click.utils import LazyFile
8
8
  from junit_xml import TestCase, TestSuite, to_xml_report_file
9
9
 
10
10
  from schemathesis.cli.commands.run.context import ExecutionContext, GroupedFailures
@@ -15,7 +15,7 @@ from schemathesis.engine import Status, events
15
15
 
16
16
  @dataclass
17
17
  class JunitXMLHandler(EventHandler):
18
- file_handle: LazyFile
18
+ path: Path
19
19
  test_cases: dict = field(default_factory=dict)
20
20
 
21
21
  def handle_event(self, ctx: ExecutionContext, event: events.EngineEvent) -> None:
@@ -34,7 +34,8 @@ class JunitXMLHandler(EventHandler):
34
34
  test_suites = [
35
35
  TestSuite("schemathesis", test_cases=list(self.test_cases.values()), hostname=platform.node())
36
36
  ]
37
- to_xml_report_file(file_descriptor=self.file_handle, test_suites=test_suites, prettyprint=True)
37
+ with open(self.path, "w") as fd:
38
+ to_xml_report_file(file_descriptor=fd, test_suites=test_suites, prettyprint=True)
38
39
 
39
40
  def get_or_create_test_case(self, label: str) -> TestCase:
40
41
  return self.test_cases.setdefault(label, TestCase(label, elapsed_sec=0.0, allow_multiple_subelements=True))
@@ -47,7 +48,7 @@ def add_failure(test_case: TestCase, checks: Iterable[GroupedFailures], context:
47
48
  response=group.response,
48
49
  failures=group.failures,
49
50
  curl=group.code_sample,
50
- config=context.output_config,
51
+ config=context.config.output,
51
52
  )
52
53
  for idx, group in enumerate(checks, 1)
53
54
  ]
@@ -13,21 +13,19 @@ import click
13
13
  from schemathesis.cli.commands.run.context import ExecutionContext, GroupedFailures
14
14
  from schemathesis.cli.commands.run.events import LoadingFinished, LoadingStarted
15
15
  from schemathesis.cli.commands.run.handlers.base import EventHandler
16
- from schemathesis.cli.commands.run.reports import ReportConfig, ReportFormat
17
16
  from schemathesis.cli.constants import ISSUE_TRACKER_URL
18
17
  from schemathesis.cli.core import get_terminal_width
18
+ from schemathesis.config import ProjectConfig, ReportFormat
19
19
  from schemathesis.core.errors import LoaderError, LoaderErrorKind, format_exception, split_traceback
20
20
  from schemathesis.core.failures import MessageBlock, Severity, format_failures
21
21
  from schemathesis.core.output import prepare_response_payload
22
22
  from schemathesis.core.result import Err, Ok
23
23
  from schemathesis.core.version import SCHEMATHESIS_VERSION
24
24
  from schemathesis.engine import Status, events
25
- from schemathesis.engine.config import EngineConfig
26
25
  from schemathesis.engine.errors import EngineErrorInfo
27
26
  from schemathesis.engine.phases import PhaseName, PhaseSkipReason
28
27
  from schemathesis.engine.phases.probes import ProbeOutcome
29
28
  from schemathesis.engine.recorder import Interaction, ScenarioRecorder
30
- from schemathesis.experimental import GLOBAL_EXPERIMENTS
31
29
  from schemathesis.generation.modes import GenerationMode
32
30
  from schemathesis.schemas import ApiStatistic
33
31
 
@@ -100,7 +98,7 @@ def display_failures_for_single_test(ctx: ExecutionContext, label: str, checks:
100
98
  failures=group.failures,
101
99
  curl=group.code_sample,
102
100
  formatter=failure_formatter,
103
- config=ctx.output_config,
101
+ config=ctx.config.output,
104
102
  )
105
103
  )
106
104
  click.echo()
@@ -770,12 +768,7 @@ def format_duration(duration_ms: int) -> str:
770
768
 
771
769
  @dataclass
772
770
  class OutputHandler(EventHandler):
773
- workers_num: int
774
- # Seed can be absent in the deterministic mode
775
- seed: int | None
776
- rate_limit: str | None
777
- wait_for_schema: float | None
778
- engine_config: EngineConfig
771
+ config: ProjectConfig
779
772
 
780
773
  loading_manager: LoadingProgressManager | None = None
781
774
  probing_manager: ProbingProgressManager | None = None
@@ -784,7 +777,6 @@ class OutputHandler(EventHandler):
784
777
 
785
778
  statistic: ApiStatistic | None = None
786
779
  skip_reasons: list[str] = field(default_factory=list)
787
- report_config: ReportConfig | None = None
788
780
  warnings: WarningData = field(default_factory=WarningData)
789
781
  errors: set[events.NonFatalError] = field(default_factory=set)
790
782
  phases: dict[PhaseName, tuple[Status, PhaseSkipReason | None]] = field(
@@ -836,6 +828,8 @@ class OutputHandler(EventHandler):
836
828
  from rich.style import Style
837
829
  from rich.table import Table
838
830
 
831
+ self.config = event.config
832
+
839
833
  assert self.loading_manager is not None
840
834
  self.loading_manager.stop()
841
835
 
@@ -1068,7 +1062,7 @@ class OutputHandler(EventHandler):
1068
1062
 
1069
1063
  if (
1070
1064
  event.status == Status.SUCCESS
1071
- and GenerationMode.POSITIVE in self.engine_config.execution.generation.modes
1065
+ and GenerationMode.POSITIVE in self.config.generation_for(operation=None, phase=event.phase.name).modes
1072
1066
  and all_positive_are_rejected(event.recorder)
1073
1067
  and statistic.should_warn_about_only_4xx()
1074
1068
  ):
@@ -1097,6 +1091,7 @@ class OutputHandler(EventHandler):
1097
1091
  )
1098
1092
  self.console.print(message)
1099
1093
  self.console.print()
1094
+ self.probing_manager = None
1100
1095
 
1101
1096
  def _on_fatal_error(self, ctx: ExecutionContext, event: events.FatalError) -> None:
1102
1097
  from rich.padding import Padding
@@ -1116,7 +1111,9 @@ class OutputHandler(EventHandler):
1116
1111
  self.console.print(Padding(Text(extra), (0, 0, 0, 5)))
1117
1112
  self.console.print()
1118
1113
 
1119
- if not (event.exception.kind == LoaderErrorKind.CONNECTION_OTHER and self.wait_for_schema is not None):
1114
+ if not (
1115
+ event.exception.kind == LoaderErrorKind.CONNECTION_OTHER and self.config.wait_for_schema is not None
1116
+ ):
1120
1117
  suggestion = LOADER_ERROR_SUGGESTIONS.get(event.exception.kind)
1121
1118
  if suggestion is not None:
1122
1119
  click.echo(_style(f"{click.style('Tip:', bold=True, fg='green')} {suggestion}"))
@@ -1134,7 +1131,7 @@ class OutputHandler(EventHandler):
1134
1131
  if not (
1135
1132
  isinstance(event.exception, LoaderError)
1136
1133
  and event.exception.kind == LoaderErrorKind.CONNECTION_OTHER
1137
- and self.wait_for_schema is not None
1134
+ and self.config.wait_for_schema is not None
1138
1135
  ):
1139
1136
  click.echo(_style(f"\n{click.style('Tip:', bold=True, fg='green')} {suggestion}"))
1140
1137
 
@@ -1195,25 +1192,6 @@ class OutputHandler(EventHandler):
1195
1192
  click.echo(_style("Check base URL or adjust data generation settings", fg="yellow"))
1196
1193
  click.echo()
1197
1194
 
1198
- def display_experiments(self) -> None:
1199
- display_section_name("EXPERIMENTS")
1200
-
1201
- click.echo()
1202
- for experiment in sorted(GLOBAL_EXPERIMENTS.enabled, key=lambda e: e.name):
1203
- click.echo(_style(f"🧪 {experiment.name}: ", bold=True), nl=False)
1204
- click.echo(_style(experiment.description))
1205
- click.echo(_style(f" Feedback: {experiment.discussion_url}"))
1206
- click.echo()
1207
-
1208
- click.echo(
1209
- _style(
1210
- "Your feedback is crucial for experimental features. "
1211
- "Please visit the provided URL(s) to share your thoughts.",
1212
- dim=True,
1213
- )
1214
- )
1215
- click.echo()
1216
-
1217
1195
  def display_stateful_failures(self, ctx: ExecutionContext) -> None:
1218
1196
  display_section_name("Stateful tests")
1219
1197
 
@@ -1255,7 +1233,7 @@ class OutputHandler(EventHandler):
1255
1233
  click.echo(f"\n{indent}<EMPTY>")
1256
1234
  else:
1257
1235
  try:
1258
- payload = prepare_response_payload(response.text, config=ctx.output_config)
1236
+ payload = prepare_response_payload(response.text, config=ctx.config.output)
1259
1237
  click.echo(textwrap.indent(f"\n{payload}", prefix=indent))
1260
1238
  except UnicodeDecodeError:
1261
1239
  click.echo(f"\n{indent}<BINARY>")
@@ -1410,24 +1388,27 @@ class OutputHandler(EventHandler):
1410
1388
  display_section_name(message, fg=color)
1411
1389
 
1412
1390
  def display_reports(self) -> None:
1413
- if self.report_config is not None:
1414
- reports = [
1415
- (format.value.upper(), self.report_config.get_path(format).name)
1416
- for format in ReportFormat
1417
- if format in self.report_config.formats
1418
- ]
1419
-
1391
+ reports = self.config.reports
1392
+ if reports.vcr.enabled or reports.har.enabled or reports.junit.enabled:
1420
1393
  click.echo(_style("Reports:", bold=True))
1421
- for report_type, path in reports:
1422
- click.echo(_style(f" - {report_type}: {path}"))
1394
+ for format, report in (
1395
+ (ReportFormat.JUNIT, reports.junit),
1396
+ (ReportFormat.VCR, reports.vcr),
1397
+ (ReportFormat.HAR, reports.har),
1398
+ ):
1399
+ if report.enabled:
1400
+ path = reports.get_path(format)
1401
+ click.echo(_style(f" - {format.value.upper()}: {path}"))
1423
1402
  click.echo()
1424
1403
 
1425
1404
  def display_seed(self) -> None:
1426
1405
  click.echo(_style("Seed: ", bold=True), nl=False)
1427
- if self.seed is None:
1406
+ # Deterministic mode can be applied to a subset of tests, but we only care if it is enabled everywhere
1407
+ # If not everywhere, then the seed matter and should be displayed
1408
+ if self.config.seed is None or self.config.generation.deterministic:
1428
1409
  click.echo("not used in the deterministic mode")
1429
1410
  else:
1430
- click.echo(str(self.seed))
1411
+ click.echo(str(self.config.seed))
1431
1412
  click.echo()
1432
1413
 
1433
1414
  def _on_engine_finished(self, ctx: ExecutionContext, event: events.EngineFinished) -> None:
@@ -1450,8 +1431,6 @@ class OutputHandler(EventHandler):
1450
1431
  display_failures(ctx)
1451
1432
  if self.warnings.missing_auth or self.warnings.only_4xx_responses:
1452
1433
  self.display_warnings()
1453
- if GLOBAL_EXPERIMENTS.enabled:
1454
- self.display_experiments()
1455
1434
  if ctx.statistic.extraction_failures:
1456
1435
  self.display_stateful_failures(ctx)
1457
1436
  display_section_name("SUMMARY")