schemathesis 3.35.0__py3-none-any.whl → 3.35.2__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.
@@ -226,6 +226,7 @@ http_interactions:"""
226
226
  for interaction in item.interactions:
227
227
  status = interaction.status.name.upper()
228
228
  # Body payloads are handled via separate `stream.write` calls to avoid some allocations
229
+ phase = f"'{interaction.phase.value}'" if interaction.phase is not None else "null"
229
230
  stream.write(
230
231
  f"""\n- id: '{current_id}'
231
232
  status: '{status}'
@@ -233,6 +234,7 @@ http_interactions:"""
233
234
  thread_id: {item.thread_id}
234
235
  correlation_id: '{item.correlation_id}'
235
236
  data_generation_method: '{interaction.data_generation_method.value}'
237
+ phase: {phase}
236
238
  elapsed: '{interaction.response.elapsed}'
237
239
  recorded_at: '{interaction.recorded_at}'
238
240
  checks:
@@ -4,7 +4,7 @@ import os
4
4
  import shutil
5
5
  from dataclasses import dataclass, field
6
6
  from queue import Queue
7
- from typing import TYPE_CHECKING
7
+ from typing import TYPE_CHECKING, Generator
8
8
 
9
9
  from ..code_samples import CodeSampleStyle
10
10
  from ..internal.deprecation import deprecated_property
@@ -60,11 +60,15 @@ class ExecutionContext:
60
60
  analysis: Result[AnalysisResult, Exception] | None = None
61
61
  output_config: OutputConfig = field(default_factory=OutputConfig)
62
62
  state_machine_sink: StateMachineSink | None = None
63
- summary_lines: list[str] = field(default_factory=list)
63
+ initialization_lines: list[str | Generator[str, None, None]] = field(default_factory=list)
64
+ summary_lines: list[str | Generator[str, None, None]] = field(default_factory=list)
64
65
 
65
66
  @deprecated_property(removed_in="4.0", replacement="show_trace")
66
67
  def show_errors_tracebacks(self) -> bool:
67
68
  return self.show_trace
68
69
 
69
- def add_summary_line(self, line: str) -> None:
70
+ def add_initialization_line(self, line: str | Generator[str, None, None]) -> None:
71
+ self.initialization_lines.append(line)
72
+
73
+ def add_summary_line(self, line: str | Generator[str, None, None]) -> None:
70
74
  self.summary_lines.append(line)
@@ -7,6 +7,7 @@ import textwrap
7
7
  import time
8
8
  from importlib import metadata
9
9
  from queue import Queue
10
+ from types import GeneratorType
10
11
  from typing import TYPE_CHECKING, Any, Generator, Literal, cast
11
12
 
12
13
  import click
@@ -776,6 +777,8 @@ def handle_initialized(context: ExecutionContext, event: events.Initialized) ->
776
777
  click.secho(f"Collected API links: {links_count}", bold=True)
777
778
  if isinstance(context.report, ServiceReportContext):
778
779
  click.secho("Report to Schemathesis.io: ENABLED", bold=True)
780
+ if context.initialization_lines:
781
+ _print_lines(context.initialization_lines)
779
782
 
780
783
 
781
784
  def handle_before_probing(context: ExecutionContext, event: events.BeforeProbing) -> None:
@@ -852,12 +855,20 @@ def handle_finished(context: ExecutionContext, event: events.Finished) -> None:
852
855
  display_statistic(context, event)
853
856
  if context.summary_lines:
854
857
  click.echo()
855
- for line in context.summary_lines:
856
- click.echo(line)
858
+ _print_lines(context.summary_lines)
857
859
  click.echo()
858
860
  display_summary(event)
859
861
 
860
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
+
861
872
  def handle_interrupted(context: ExecutionContext, event: events.Interrupted) -> None:
862
873
  click.echo()
863
874
  _handle_interrupted(context)
@@ -1,17 +1,19 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
- from contextlib import suppress
4
+ from contextlib import contextmanager, suppress
5
5
  from dataclasses import dataclass, field
6
6
  from functools import lru_cache
7
- from typing import Any, Generator, Set, TypeVar, cast
7
+ from typing import Any, Generator, Set, Type, TypeVar, cast
8
8
 
9
9
  import jsonschema
10
10
  from hypothesis import strategies as st
11
- from hypothesis.errors import Unsatisfiable
11
+ from hypothesis.errors import InvalidArgument, Unsatisfiable
12
12
  from hypothesis_jsonschema import from_schema
13
13
  from hypothesis_jsonschema._canonicalise import canonicalish
14
14
 
15
+ from schemathesis.constants import NOT_SET
16
+
15
17
  from ._hypothesis import combine_strategies, get_single_example
16
18
  from ._methods import DataGenerationMethod
17
19
 
@@ -29,6 +31,26 @@ UNKNOWN_PROPERTY_KEY = "x-schemathesis-unknown-property"
29
31
  UNKNOWN_PROPERTY_VALUE = 42
30
32
 
31
33
 
34
+ @dataclass
35
+ class GeneratedValue:
36
+ value: Any
37
+ data_generation_method: DataGenerationMethod
38
+
39
+ __slots__ = ("value", "data_generation_method")
40
+
41
+ @classmethod
42
+ def with_positive(cls, value: Any) -> GeneratedValue:
43
+ return cls(value, DataGenerationMethod.positive)
44
+
45
+ @classmethod
46
+ def with_negative(cls, value: Any) -> GeneratedValue:
47
+ return cls(value, DataGenerationMethod.negative)
48
+
49
+
50
+ PositiveValue = GeneratedValue.with_positive
51
+ NegativeValue = GeneratedValue.with_negative
52
+
53
+
32
54
  @lru_cache(maxsize=128)
33
55
  def cached_draw(strategy: st.SearchStrategy) -> Any:
34
56
  return get_single_example(strategy)
@@ -67,11 +89,9 @@ def _to_hashable_key(value: T) -> T | tuple[type, str]:
67
89
  return value
68
90
 
69
91
 
70
- def cover_schema(ctx: CoverageContext, schema: dict) -> list:
71
- return list(cover_schema_iter(ctx, schema))
72
-
73
-
74
- def _cover_positive_for_type(ctx: CoverageContext, schema: dict, ty: str | None) -> Generator:
92
+ def _cover_positive_for_type(
93
+ ctx: CoverageContext, schema: dict, ty: str | None
94
+ ) -> Generator[GeneratedValue, None, None]:
75
95
  if ty == "object" or ty == "array":
76
96
  template_schema = _get_template_schema(schema, ty)
77
97
  template = ctx.generate_from_schema(template_schema)
@@ -79,8 +99,8 @@ def _cover_positive_for_type(ctx: CoverageContext, schema: dict, ty: str | None)
79
99
  template = None
80
100
  if DataGenerationMethod.positive in ctx.data_generation_methods:
81
101
  ctx = ctx.with_positive()
82
- enum = schema.get("enum")
83
- const = schema.get("const")
102
+ enum = schema.get("enum", NOT_SET)
103
+ const = schema.get("const", NOT_SET)
84
104
  for key in ("anyOf", "oneOf"):
85
105
  sub_schemas = schema.get(key)
86
106
  if sub_schemas is not None:
@@ -91,43 +111,70 @@ def _cover_positive_for_type(ctx: CoverageContext, schema: dict, ty: str | None)
91
111
  if len(all_of) == 1:
92
112
  yield from cover_schema_iter(ctx, all_of[0])
93
113
  else:
94
- canonical = canonicalish(schema)
95
- yield from cover_schema_iter(ctx, canonical)
96
- if enum is not None:
97
- yield from enum
98
- elif const is not None:
99
- yield const
114
+ with suppress(jsonschema.SchemaError):
115
+ canonical = canonicalish(schema)
116
+ yield from cover_schema_iter(ctx, canonical)
117
+ if enum is not NOT_SET:
118
+ for value in enum:
119
+ yield PositiveValue(value)
120
+ elif const is not NOT_SET:
121
+ yield PositiveValue(const)
100
122
  elif ty is not None:
101
123
  if ty == "null":
102
- yield None
103
- if ty == "boolean":
104
- yield True
105
- yield False
106
- if ty == "string":
124
+ yield PositiveValue(None)
125
+ elif ty == "boolean":
126
+ yield PositiveValue(True)
127
+ yield PositiveValue(False)
128
+ elif ty == "string":
107
129
  yield from _positive_string(ctx, schema)
108
- if ty == "integer" or ty == "number":
130
+ elif ty == "integer" or ty == "number":
109
131
  yield from _positive_number(ctx, schema)
110
- if ty == "array":
132
+ elif ty == "array":
111
133
  yield from _positive_array(ctx, schema, cast(list, template))
112
- if ty == "object":
134
+ elif ty == "object":
113
135
  yield from _positive_object(ctx, schema, cast(dict, template))
114
136
 
115
137
 
116
- def cover_schema_iter(ctx: CoverageContext, schema: dict) -> Generator:
117
- types = schema.get("type", [])
138
+ @contextmanager
139
+ def _ignore_unfixable(
140
+ *,
141
+ # Cache exception types here as `jsonschema` uses a custom `__getattr__` on the module level
142
+ # and it may cause errors during the interpreter shutdown
143
+ ref_error: Type[Exception] = jsonschema.RefResolutionError,
144
+ schema_error: Type[Exception] = jsonschema.SchemaError,
145
+ ) -> Generator:
146
+ try:
147
+ yield
148
+ except (Unsatisfiable, ref_error, schema_error):
149
+ pass
150
+ except InvalidArgument as exc:
151
+ message = str(exc)
152
+ if "Cannot create non-empty" not in message and "is not in the specified alphabet" not in message:
153
+ raise
154
+ except TypeError as exc:
155
+ if "first argument must be string or compiled pattern" not in str(exc):
156
+ raise
157
+
158
+
159
+ def cover_schema_iter(ctx: CoverageContext, schema: dict | bool) -> Generator[GeneratedValue, None, None]:
160
+ if isinstance(schema, bool):
161
+ types = ["null", "boolean", "string", "number", "array", "object"]
162
+ schema = {}
163
+ else:
164
+ types = schema.get("type", [])
118
165
  if not isinstance(types, list):
119
- types = [types]
166
+ types = [types] # type: ignore[unreachable]
120
167
  if not types:
121
- with suppress(Unsatisfiable, jsonschema.RefResolutionError):
168
+ with _ignore_unfixable():
122
169
  yield from _cover_positive_for_type(ctx, schema, None)
123
170
  for ty in types:
124
- with suppress(Unsatisfiable, jsonschema.RefResolutionError):
171
+ with _ignore_unfixable():
125
172
  yield from _cover_positive_for_type(ctx, schema, ty)
126
173
  if DataGenerationMethod.negative in ctx.data_generation_methods:
127
174
  template = None
128
175
  seen: Set[Any | tuple[type, str]] = set()
129
176
  for key, value in schema.items():
130
- with suppress(Unsatisfiable, jsonschema.RefResolutionError):
177
+ with _ignore_unfixable():
131
178
  if key == "enum":
132
179
  yield from _negative_enum(ctx, value)
133
180
  elif key == "const":
@@ -143,21 +190,27 @@ def cover_schema_iter(ctx: CoverageContext, schema: dict) -> Generator:
143
190
  yield from _negative_format(ctx, schema, value)
144
191
  elif key == "maximum":
145
192
  next = value + 1
146
- yield next
193
+ yield NegativeValue(next)
147
194
  seen.add(next)
148
195
  elif key == "minimum":
149
196
  next = value - 1
150
- yield next
197
+ yield NegativeValue(next)
151
198
  seen.add(next)
152
199
  elif key == "exclusiveMaximum" or key == "exclusiveMinimum" and value not in seen:
153
- yield value
200
+ yield NegativeValue(value)
154
201
  seen.add(value)
155
202
  elif key == "multipleOf":
156
203
  yield from _negative_multiple_of(ctx, schema, value)
157
204
  elif key == "minLength" and 0 < value < BUFFER_SIZE and "pattern" not in schema:
158
- yield ctx.generate_from_schema({**schema, "minLength": value - 1, "maxLength": value - 1})
205
+ with suppress(InvalidArgument):
206
+ yield NegativeValue(
207
+ ctx.generate_from_schema({**schema, "minLength": value - 1, "maxLength": value - 1})
208
+ )
159
209
  elif key == "maxLength" and value < BUFFER_SIZE and "pattern" not in schema:
160
- yield ctx.generate_from_schema({**schema, "minLength": value + 1, "maxLength": value + 1})
210
+ with suppress(InvalidArgument):
211
+ yield NegativeValue(
212
+ ctx.generate_from_schema({**schema, "minLength": value + 1, "maxLength": value + 1})
213
+ )
161
214
  elif key == "uniqueItems" and value:
162
215
  yield from _negative_unique_items(ctx, schema)
163
216
  elif key == "required":
@@ -165,14 +218,15 @@ def cover_schema_iter(ctx: CoverageContext, schema: dict) -> Generator:
165
218
  yield from _negative_required(ctx, template, value)
166
219
  elif key == "additionalProperties" and not value:
167
220
  template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
168
- yield {**template, UNKNOWN_PROPERTY_KEY: UNKNOWN_PROPERTY_VALUE}
221
+ yield NegativeValue({**template, UNKNOWN_PROPERTY_KEY: UNKNOWN_PROPERTY_VALUE})
169
222
  elif key == "allOf":
170
223
  nctx = ctx.with_negative()
171
224
  if len(value) == 1:
172
225
  yield from cover_schema_iter(nctx, value[0])
173
226
  else:
174
- canonical = canonicalish(schema)
175
- yield from cover_schema_iter(nctx, canonical)
227
+ with _ignore_unfixable():
228
+ canonical = canonicalish(schema)
229
+ yield from cover_schema_iter(nctx, canonical)
176
230
  elif key == "anyOf" or key == "oneOf":
177
231
  nctx = ctx.with_negative()
178
232
  # NOTE: Other sub-schemas are not filtered out
@@ -180,6 +234,15 @@ def cover_schema_iter(ctx: CoverageContext, schema: dict) -> Generator:
180
234
  yield from cover_schema_iter(nctx, sub_schema)
181
235
 
182
236
 
237
+ def _get_properties(schema: dict | bool) -> dict | bool:
238
+ if isinstance(schema, dict):
239
+ if "example" in schema:
240
+ return {"const": schema["example"]}
241
+ if schema.get("type") == "object":
242
+ return _get_template_schema(schema, "object")
243
+ return schema
244
+
245
+
183
246
  def _get_template_schema(schema: dict, ty: str) -> dict:
184
247
  if ty == "object":
185
248
  properties = schema.get("properties")
@@ -188,41 +251,40 @@ def _get_template_schema(schema: dict, ty: str) -> dict:
188
251
  **schema,
189
252
  "required": list(properties),
190
253
  "type": ty,
191
- "properties": {
192
- k: _get_template_schema(v, "object") if v.get("type") == "object" else v
193
- for k, v in properties.items()
194
- },
254
+ "properties": {k: _get_properties(v) for k, v in properties.items()},
195
255
  }
196
256
  return {**schema, "type": ty}
197
257
 
198
258
 
199
- def _positive_string(ctx: CoverageContext, schema: dict) -> Generator:
259
+ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
200
260
  """Generate positive string values."""
201
261
  # Boundary and near boundary values
202
262
  min_length = schema.get("minLength")
203
263
  max_length = schema.get("maxLength")
204
264
 
205
- if not min_length and not max_length:
265
+ if "example" in schema:
266
+ yield PositiveValue(schema["example"])
267
+ elif not min_length and not max_length:
206
268
  # Default positive value
207
- yield ctx.generate_from_schema(schema)
269
+ yield PositiveValue(ctx.generate_from_schema(schema))
208
270
 
209
271
  seen = set()
210
272
 
211
273
  if min_length is not None and min_length < BUFFER_SIZE and "pattern" not in schema:
212
274
  # Exactly the minimum length
213
- yield ctx.generate_from_schema({**schema, "maxLength": min_length})
275
+ yield PositiveValue(ctx.generate_from_schema({**schema, "maxLength": min_length}))
214
276
  seen.add(min_length)
215
277
 
216
278
  # One character more than minimum if possible
217
279
  larger = min_length + 1
218
280
  if larger < BUFFER_SIZE and larger not in seen and (not max_length or larger <= max_length):
219
- yield ctx.generate_from_schema({**schema, "minLength": larger, "maxLength": larger})
281
+ yield PositiveValue(ctx.generate_from_schema({**schema, "minLength": larger, "maxLength": larger}))
220
282
  seen.add(larger)
221
283
 
222
284
  if max_length is not None and "pattern" not in schema:
223
285
  # Exactly the maximum length
224
286
  if max_length < BUFFER_SIZE and max_length not in seen:
225
- yield ctx.generate_from_schema({**schema, "minLength": max_length})
287
+ yield PositiveValue(ctx.generate_from_schema({**schema, "minLength": max_length}))
226
288
  seen.add(max_length)
227
289
 
228
290
  # One character less than maximum if possible
@@ -232,7 +294,7 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator:
232
294
  and smaller not in seen
233
295
  and (smaller > 0 and (min_length is None or smaller >= min_length))
234
296
  ):
235
- yield ctx.generate_from_schema({**schema, "minLength": smaller, "maxLength": smaller})
297
+ yield PositiveValue(ctx.generate_from_schema({**schema, "minLength": smaller, "maxLength": smaller}))
236
298
  seen.add(smaller)
237
299
 
238
300
 
@@ -244,7 +306,7 @@ def closest_multiple_greater_than(y: int, x: int) -> int:
244
306
  return x * (quotient + 1)
245
307
 
246
308
 
247
- def _positive_number(ctx: CoverageContext, schema: dict) -> Generator:
309
+ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
248
310
  """Generate positive integer values."""
249
311
  # Boundary and near boundary values
250
312
  minimum = schema.get("minimum")
@@ -257,9 +319,11 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator:
257
319
  maximum = exclusive_maximum - 1
258
320
  multiple_of = schema.get("multipleOf")
259
321
 
260
- if not minimum and not maximum:
322
+ if "example" in schema:
323
+ yield PositiveValue(schema["example"])
324
+ elif not minimum and not maximum:
261
325
  # Default positive value
262
- yield ctx.generate_from_schema(schema)
326
+ yield PositiveValue(ctx.generate_from_schema(schema))
263
327
 
264
328
  seen = set()
265
329
 
@@ -270,7 +334,7 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator:
270
334
  else:
271
335
  smallest = minimum
272
336
  seen.add(smallest)
273
- yield smallest
337
+ yield PositiveValue(smallest)
274
338
 
275
339
  # One more than minimum if possible
276
340
  if multiple_of is not None:
@@ -279,7 +343,7 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator:
279
343
  larger = minimum + 1
280
344
  if larger not in seen and (not maximum or larger <= maximum):
281
345
  seen.add(larger)
282
- yield larger
346
+ yield PositiveValue(larger)
283
347
 
284
348
  if maximum is not None:
285
349
  # Exactly the maximum
@@ -289,7 +353,7 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator:
289
353
  largest = maximum
290
354
  if largest not in seen:
291
355
  seen.add(largest)
292
- yield largest
356
+ yield PositiveValue(largest)
293
357
 
294
358
  # One less than maximum if possible
295
359
  if multiple_of is not None:
@@ -298,12 +362,15 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator:
298
362
  smaller = maximum - 1
299
363
  if smaller not in seen and (smaller > 0 and (minimum is None or smaller >= minimum)):
300
364
  seen.add(smaller)
301
- yield smaller
365
+ yield PositiveValue(smaller)
302
366
 
303
367
 
304
- def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Generator:
368
+ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Generator[GeneratedValue, None, None]:
305
369
  seen = set()
306
- yield template
370
+ if "example" in schema:
371
+ yield PositiveValue(schema["example"])
372
+ else:
373
+ yield PositiveValue(template)
307
374
  seen.add(len(template))
308
375
 
309
376
  # Boundary and near-boundary sizes
@@ -315,99 +382,102 @@ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Gener
315
382
  # One item more than minimum if possible
316
383
  larger = min_items + 1
317
384
  if larger not in seen and (max_items is None or larger <= max_items):
318
- yield ctx.generate_from_schema({**schema, "minItems": larger, "maxItems": larger})
385
+ yield PositiveValue(ctx.generate_from_schema({**schema, "minItems": larger, "maxItems": larger}))
319
386
  seen.add(larger)
320
387
 
321
388
  if max_items is not None:
322
- if max_items not in seen:
323
- yield ctx.generate_from_schema({**schema, "minItems": max_items})
389
+ if max_items < BUFFER_SIZE and max_items not in seen:
390
+ yield PositiveValue(ctx.generate_from_schema({**schema, "minItems": max_items}))
324
391
  seen.add(max_items)
325
392
 
326
393
  # One item smaller than maximum if possible
327
394
  smaller = max_items - 1
328
- if smaller > 0 and smaller not in seen and (min_items is None or smaller >= min_items):
329
- yield ctx.generate_from_schema({**schema, "minItems": smaller, "maxItems": smaller})
395
+ if (
396
+ smaller < BUFFER_SIZE
397
+ and smaller > 0
398
+ and smaller not in seen
399
+ and (min_items is None or smaller >= min_items)
400
+ ):
401
+ yield PositiveValue(ctx.generate_from_schema({**schema, "minItems": smaller, "maxItems": smaller}))
330
402
  seen.add(smaller)
331
403
 
332
404
 
333
- def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Generator:
334
- yield template
405
+ def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Generator[GeneratedValue, None, None]:
406
+ if "example" in schema:
407
+ yield PositiveValue(schema["example"])
408
+ else:
409
+ yield PositiveValue(template)
335
410
  # Only required properties
336
411
  properties = schema.get("properties", {})
337
412
  if set(properties) != set(schema.get("required", {})):
338
413
  only_required = {k: v for k, v in template.items() if k in schema.get("required", [])}
339
- yield only_required
414
+ yield PositiveValue(only_required)
340
415
  seen = set()
341
416
  for name, sub_schema in properties.items():
342
417
  seen.add(_to_hashable_key(template.get(name)))
343
418
  for new in cover_schema_iter(ctx, sub_schema):
344
- key = _to_hashable_key(new)
419
+ key = _to_hashable_key(new.value)
345
420
  if key not in seen:
346
- yield {**template, name: new}
421
+ yield PositiveValue({**template, name: new.value})
347
422
  seen.add(key)
348
423
  seen.clear()
349
424
 
350
425
 
351
- @lru_cache(maxsize=128)
352
- def _get_negative_enum_strategy(value: tuple) -> st.SearchStrategy:
353
- return JSON_STRATEGY.filter(lambda x: x not in value)
354
-
355
-
356
- def _negative_enum(ctx: CoverageContext, value: list) -> Generator:
357
- try:
358
- strategy = _get_negative_enum_strategy(tuple(value))
359
- except TypeError:
360
- # The value is not hashable
361
- strategy = JSON_STRATEGY.filter(lambda x: x not in value)
426
+ def _negative_enum(ctx: CoverageContext, value: list) -> Generator[GeneratedValue, None, None]:
427
+ strategy = JSON_STRATEGY.filter(lambda x: x not in value)
362
428
  # The exact negative value is not important here
363
- yield ctx.generate_from(strategy, cached=True)
429
+ yield NegativeValue(ctx.generate_from(strategy, cached=True))
364
430
 
365
431
 
366
- def _negative_properties(ctx: CoverageContext, template: dict, properties: dict) -> Generator:
432
+ def _negative_properties(
433
+ ctx: CoverageContext, template: dict, properties: dict
434
+ ) -> Generator[GeneratedValue, None, None]:
367
435
  nctx = ctx.with_negative()
368
436
  for key, sub_schema in properties.items():
369
437
  for value in cover_schema_iter(nctx, sub_schema):
370
- yield {**template, key: value}
371
-
372
-
373
- @lru_cache(maxsize=128)
374
- def _get_negative_pattern_strategy(value: str) -> st.SearchStrategy:
375
- return st.text().filter(lambda x: x != value)
438
+ yield NegativeValue({**template, key: value.value})
376
439
 
377
440
 
378
- def _negative_pattern(ctx: CoverageContext, pattern: str) -> Generator:
379
- yield ctx.generate_from(_get_negative_pattern_strategy(pattern), cached=True)
441
+ def _negative_pattern(ctx: CoverageContext, pattern: str) -> Generator[GeneratedValue, None, None]:
442
+ yield NegativeValue(ctx.generate_from(st.text().filter(lambda x: x != pattern), cached=True))
380
443
 
381
444
 
382
445
  def _with_negated_key(schema: dict, key: str, value: Any) -> dict:
383
446
  return {"allOf": [{k: v for k, v in schema.items() if k != key}, {"not": {key: value}}]}
384
447
 
385
448
 
386
- def _negative_multiple_of(ctx: CoverageContext, schema: dict, multiple_of: int | float) -> Generator:
387
- yield ctx.generate_from_schema(_with_negated_key(schema, "multipleOf", multiple_of))
449
+ def _negative_multiple_of(
450
+ ctx: CoverageContext, schema: dict, multiple_of: int | float
451
+ ) -> Generator[GeneratedValue, None, None]:
452
+ yield NegativeValue(ctx.generate_from_schema(_with_negated_key(schema, "multipleOf", multiple_of)))
388
453
 
389
454
 
390
- def _negative_unique_items(ctx: CoverageContext, schema: dict) -> Generator:
455
+ def _negative_unique_items(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
391
456
  unique = ctx.generate_from_schema({**schema, "type": "array", "minItems": 1, "maxItems": 1})
392
- yield unique + unique
457
+ yield NegativeValue(unique + unique)
393
458
 
394
459
 
395
- def _negative_required(ctx: CoverageContext, template: dict, required: list[str]) -> Generator:
460
+ def _negative_required(
461
+ ctx: CoverageContext, template: dict, required: list[str]
462
+ ) -> Generator[GeneratedValue, None, None]:
396
463
  for key in required:
397
- yield {k: v for k, v in template.items() if k != key}
464
+ yield NegativeValue({k: v for k, v in template.items() if k != key})
398
465
 
399
466
 
400
- def _negative_format(ctx: CoverageContext, schema: dict, format: str) -> Generator:
467
+ def _negative_format(ctx: CoverageContext, schema: dict, format: str) -> Generator[GeneratedValue, None, None]:
401
468
  # Hypothesis-jsonschema does not canonicalise it properly right now, which leads to unsatisfiable schema
402
469
  without_format = {k: v for k, v in schema.items() if k != "format"}
403
470
  without_format.setdefault("type", "string")
404
471
  strategy = from_schema(without_format)
405
472
  if format in jsonschema.Draft202012Validator.FORMAT_CHECKER.checkers:
406
- strategy = strategy.filter(lambda v: not jsonschema.Draft202012Validator.FORMAT_CHECKER.conforms(v, format))
407
- yield ctx.generate_from(strategy)
473
+ strategy = strategy.filter(
474
+ lambda v: (format == "hostname" and v == "")
475
+ or not jsonschema.Draft202012Validator.FORMAT_CHECKER.conforms(v, format)
476
+ )
477
+ yield NegativeValue(ctx.generate_from(strategy))
408
478
 
409
479
 
410
- def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Generator:
480
+ def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Generator[GeneratedValue, None, None]:
411
481
  strategies = {
412
482
  "integer": st.integers(),
413
483
  "number": NUMERIC_STRATEGY,
@@ -429,5 +499,5 @@ def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Gene
429
499
  strategies["number"] = FLOAT_STRATEGY.filter(lambda x: x != int(x))
430
500
  negative_strategy = combine_strategies(tuple(strategies.values())).filter(lambda x: _to_hashable_key(x) not in seen)
431
501
  value = ctx.generate_from(negative_strategy, cached=True)
432
- yield value
502
+ yield NegativeValue(value)
433
503
  seen.add(_to_hashable_key(value))
schemathesis/models.py CHANGED
@@ -125,6 +125,15 @@ def prepare_request_data(kwargs: dict[str, Any]) -> PreparedRequestData:
125
125
  )
126
126
 
127
127
 
128
+ @dataclass
129
+ class TestPhase(Enum):
130
+ __test__ = False
131
+
132
+ EXPLICIT = "explicit"
133
+ COVERAGE = "coverage"
134
+ GENERATE = "generate"
135
+
136
+
128
137
  @dataclass
129
138
  class GenerationMetadata:
130
139
  """Stores various information about how data is generated."""
@@ -134,8 +143,9 @@ class GenerationMetadata:
134
143
  headers: DataGenerationMethod | None
135
144
  cookies: DataGenerationMethod | None
136
145
  body: DataGenerationMethod | None
146
+ phase: TestPhase
137
147
 
138
- __slots__ = ("query", "path_parameters", "headers", "cookies", "body")
148
+ __slots__ = ("query", "path_parameters", "headers", "cookies", "body", "phase")
139
149
 
140
150
 
141
151
  @dataclass(repr=False)
@@ -968,6 +978,7 @@ class Interaction:
968
978
  checks: list[Check]
969
979
  status: Status
970
980
  data_generation_method: DataGenerationMethod
981
+ phase: TestPhase | None
971
982
  recorded_at: str = field(default_factory=lambda: datetime.datetime.now(TIMEZONE).isoformat())
972
983
 
973
984
  @classmethod
@@ -978,6 +989,7 @@ class Interaction:
978
989
  status=status,
979
990
  checks=checks,
980
991
  data_generation_method=cast(DataGenerationMethod, case.data_generation_method),
992
+ phase=case.meta.phase if case.meta is not None else None,
981
993
  )
982
994
 
983
995
  @classmethod
@@ -1000,6 +1012,7 @@ class Interaction:
1000
1012
  status=status,
1001
1013
  checks=checks,
1002
1014
  data_generation_method=cast(DataGenerationMethod, case.data_generation_method),
1015
+ phase=case.meta.phase if case.meta is not None else None,
1003
1016
  )
1004
1017
 
1005
1018
 
@@ -14,7 +14,7 @@ from .serialization import SerializedError, SerializedTestResult
14
14
 
15
15
  if TYPE_CHECKING:
16
16
  from ..models import APIOperation, Status, TestResult, TestResultSet
17
- from ..schemas import BaseSchema
17
+ from ..schemas import BaseSchema, Specification
18
18
  from ..service.models import AnalysisResult
19
19
  from ..stateful import events
20
20
  from . import probes
@@ -39,6 +39,7 @@ class Initialized(ExecutionEvent):
39
39
  """Runner is initialized, settings are prepared, requests session is ready."""
40
40
 
41
41
  schema: dict[str, Any]
42
+ specification: Specification
42
43
  # Total number of operations in the schema
43
44
  operations_count: int | None
44
45
  # Total number of links in the schema
@@ -71,6 +72,7 @@ class Initialized(ExecutionEvent):
71
72
  """Computes all needed data from a schema instance."""
72
73
  return cls(
73
74
  schema=schema.raw_schema,
75
+ specification=schema.specification,
74
76
  operations_count=schema.operations_count if count_operations else None,
75
77
  links_count=schema.links_count if count_links else None,
76
78
  location=schema.location,