schemathesis 4.0.0a3__py3-none-any.whl → 4.0.0a5__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 (53) hide show
  1. schemathesis/cli/__init__.py +3 -3
  2. schemathesis/cli/commands/run/__init__.py +159 -135
  3. schemathesis/cli/commands/run/checks.py +2 -3
  4. schemathesis/cli/commands/run/context.py +102 -19
  5. schemathesis/cli/commands/run/executor.py +33 -12
  6. schemathesis/cli/commands/run/filters.py +1 -0
  7. schemathesis/cli/commands/run/handlers/cassettes.py +27 -46
  8. schemathesis/cli/commands/run/handlers/junitxml.py +1 -1
  9. schemathesis/cli/commands/run/handlers/output.py +238 -102
  10. schemathesis/cli/commands/run/hypothesis.py +14 -41
  11. schemathesis/cli/commands/run/reports.py +72 -0
  12. schemathesis/cli/commands/run/validation.py +18 -12
  13. schemathesis/cli/ext/groups.py +42 -13
  14. schemathesis/cli/ext/options.py +15 -8
  15. schemathesis/core/__init__.py +7 -1
  16. schemathesis/core/errors.py +79 -11
  17. schemathesis/core/failures.py +2 -1
  18. schemathesis/core/transforms.py +1 -1
  19. schemathesis/engine/config.py +2 -2
  20. schemathesis/engine/core.py +11 -1
  21. schemathesis/engine/errors.py +8 -3
  22. schemathesis/engine/events.py +7 -0
  23. schemathesis/engine/phases/__init__.py +16 -4
  24. schemathesis/engine/phases/stateful/_executor.py +1 -1
  25. schemathesis/engine/phases/unit/__init__.py +77 -53
  26. schemathesis/engine/phases/unit/_executor.py +28 -23
  27. schemathesis/engine/phases/unit/_pool.py +8 -0
  28. schemathesis/errors.py +6 -2
  29. schemathesis/experimental/__init__.py +0 -6
  30. schemathesis/filters.py +8 -0
  31. schemathesis/generation/coverage.py +6 -1
  32. schemathesis/generation/hypothesis/builder.py +222 -97
  33. schemathesis/generation/stateful/state_machine.py +49 -3
  34. schemathesis/openapi/checks.py +3 -1
  35. schemathesis/pytest/lazy.py +43 -5
  36. schemathesis/pytest/plugin.py +4 -4
  37. schemathesis/schemas.py +1 -1
  38. schemathesis/specs/openapi/checks.py +28 -11
  39. schemathesis/specs/openapi/examples.py +2 -5
  40. schemathesis/specs/openapi/expressions/__init__.py +22 -6
  41. schemathesis/specs/openapi/expressions/nodes.py +15 -21
  42. schemathesis/specs/openapi/expressions/parser.py +1 -1
  43. schemathesis/specs/openapi/parameters.py +0 -2
  44. schemathesis/specs/openapi/patterns.py +24 -7
  45. schemathesis/specs/openapi/schemas.py +13 -13
  46. schemathesis/specs/openapi/serialization.py +14 -0
  47. schemathesis/specs/openapi/stateful/__init__.py +96 -23
  48. schemathesis/specs/openapi/{links.py → stateful/links.py} +60 -16
  49. {schemathesis-4.0.0a3.dist-info → schemathesis-4.0.0a5.dist-info}/METADATA +7 -26
  50. {schemathesis-4.0.0a3.dist-info → schemathesis-4.0.0a5.dist-info}/RECORD +53 -52
  51. {schemathesis-4.0.0a3.dist-info → schemathesis-4.0.0a5.dist-info}/WHEEL +0 -0
  52. {schemathesis-4.0.0a3.dist-info → schemathesis-4.0.0a5.dist-info}/entry_points.txt +0 -0
  53. {schemathesis-4.0.0a3.dist-info → schemathesis-4.0.0a5.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
- import json
4
3
  from dataclasses import dataclass, field
4
+ from enum import Enum
5
5
  from functools import wraps
6
6
  from itertools import combinations
7
7
  from time import perf_counter
@@ -13,62 +13,41 @@ from hypothesis import strategies as st
13
13
  from hypothesis.errors import Unsatisfiable
14
14
  from jsonschema.exceptions import SchemaError
15
15
 
16
- from schemathesis.auths import AuthStorageMark
17
- from schemathesis.core import NOT_SET, NotSet, media_types
16
+ from schemathesis import auths
17
+ from schemathesis.auths import AuthStorage, AuthStorageMark
18
+ from schemathesis.core import NOT_SET, NotSet, SpecificationFeature, media_types
18
19
  from schemathesis.core.errors import InvalidSchema, SerializationNotPossible
19
20
  from schemathesis.core.marks import Mark
20
- from schemathesis.core.result import Ok, Result
21
21
  from schemathesis.core.transport import prepare_urlencoded
22
22
  from schemathesis.core.validation import has_invalid_characters, is_latin_1_encodable
23
- from schemathesis.experimental import COVERAGE_PHASE
24
23
  from schemathesis.generation import GenerationConfig, GenerationMode, coverage
25
24
  from schemathesis.generation.case import Case
26
25
  from schemathesis.generation.hypothesis import DEFAULT_DEADLINE, examples, setup, strategies
27
26
  from schemathesis.generation.hypothesis.given import GivenInput
28
- from schemathesis.generation.meta import CaseMetadata, CoveragePhaseData, GenerationInfo, PhaseInfo
27
+ from schemathesis.generation.meta import (
28
+ CaseMetadata,
29
+ ComponentInfo,
30
+ ComponentKind,
31
+ CoveragePhaseData,
32
+ GenerationInfo,
33
+ PhaseInfo,
34
+ )
29
35
  from schemathesis.hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookDispatcherMark
30
- from schemathesis.schemas import APIOperation, BaseSchema, ParameterSet
36
+ from schemathesis.schemas import APIOperation, ParameterSet
31
37
 
32
38
  setup()
33
39
 
34
40
 
35
- def get_all_tests(
36
- *,
37
- schema: BaseSchema,
38
- test_func: Callable,
39
- generation_config: GenerationConfig,
40
- settings: hypothesis.settings | None = None,
41
- seed: int | None = None,
42
- as_strategy_kwargs: Callable[[APIOperation], dict[str, Any]] | None = None,
43
- given_kwargs: dict[str, GivenInput] | None = None,
44
- ) -> Generator[Result[tuple[APIOperation, Callable], InvalidSchema], None, None]:
45
- """Generate all operations and Hypothesis tests for them."""
46
- for result in schema.get_all_operations(generation_config=generation_config):
47
- if isinstance(result, Ok):
48
- operation = result.ok()
49
- if callable(as_strategy_kwargs):
50
- _as_strategy_kwargs = as_strategy_kwargs(operation)
51
- else:
52
- _as_strategy_kwargs = {}
53
- test = create_test(
54
- operation=operation,
55
- test_func=test_func,
56
- config=HypothesisTestConfig(
57
- settings=settings,
58
- seed=seed,
59
- generation=generation_config,
60
- as_strategy_kwargs=_as_strategy_kwargs,
61
- given_kwargs=given_kwargs or {},
62
- ),
63
- )
64
- yield Ok((operation, test))
65
- else:
66
- yield result
41
+ class HypothesisTestMode(Enum):
42
+ EXAMPLES = "examples"
43
+ COVERAGE = "coverage"
44
+ FUZZING = "fuzzing"
67
45
 
68
46
 
69
47
  @dataclass
70
48
  class HypothesisTestConfig:
71
49
  generation: GenerationConfig
50
+ modes: list[HypothesisTestMode]
72
51
  settings: hypothesis.settings | None = None
73
52
  seed: int | None = None
74
53
  as_strategy_kwargs: dict[str, Any] = field(default_factory=dict)
@@ -124,16 +103,33 @@ def create_test(
124
103
  phases = tuple(phase for phase in settings.phases if phase != Phase.explain)
125
104
  settings = hypothesis.settings(settings, phases=phases)
126
105
 
106
+ # Remove `reuse` & `generate` phases to avoid yielding any test cases if we don't do fuzzing
107
+ if HypothesisTestMode.FUZZING not in config.modes and (
108
+ Phase.generate in settings.phases or Phase.reuse in settings.phases
109
+ ):
110
+ phases = tuple(phase for phase in settings.phases if phase not in (Phase.reuse, Phase.generate))
111
+ settings = hypothesis.settings(settings, phases=phases)
112
+
113
+ specification = operation.schema.specification
114
+
127
115
  # Add examples if explicit phase is enabled
128
- if Phase.explicit in settings.phases:
116
+ if (
117
+ HypothesisTestMode.EXAMPLES in config.modes
118
+ and Phase.explicit in settings.phases
119
+ and specification.supports_feature(SpecificationFeature.EXAMPLES)
120
+ ):
129
121
  hypothesis_test = add_examples(hypothesis_test, operation, hook_dispatcher=hook_dispatcher, **strategy_kwargs)
130
122
 
131
- if COVERAGE_PHASE.is_enabled:
132
- # Ensure explicit phase is enabled if coverage is enabled
133
- if Phase.explicit not in settings.phases:
134
- phases = settings.phases + (Phase.explicit,)
135
- settings = hypothesis.settings(settings, phases=phases)
136
- hypothesis_test = add_coverage(hypothesis_test, operation, config.generation.modes)
123
+ if (
124
+ HypothesisTestMode.COVERAGE in config.modes
125
+ and Phase.explicit in settings.phases
126
+ and specification.supports_feature(SpecificationFeature.COVERAGE)
127
+ and not config.given_args
128
+ and not config.given_kwargs
129
+ ):
130
+ hypothesis_test = add_coverage(
131
+ hypothesis_test, operation, config.generation.modes, auth_storage, config.as_strategy_kwargs
132
+ )
137
133
 
138
134
  setattr(hypothesis_test, SETTINGS_ATTRIBUTE_NAME, settings)
139
135
 
@@ -184,6 +180,7 @@ def add_examples(
184
180
  NonSerializableMark.set(test, exc)
185
181
  if isinstance(exc, SchemaError):
186
182
  InvalidRegexMark.set(test, exc)
183
+
187
184
  context = HookContext(operation) # context should be passed here instead
188
185
  GLOBAL_HOOK_DISPATCHER.dispatch("before_add_examples", context, result)
189
186
  operation.schema.hooks.dispatch("before_add_examples", context, result)
@@ -198,6 +195,7 @@ def add_examples(
198
195
  continue
199
196
  adjust_urlencoded_payload(example)
200
197
  test = hypothesis.example(case=example)(test)
198
+
201
199
  return test
202
200
 
203
201
 
@@ -211,10 +209,37 @@ def adjust_urlencoded_payload(case: Case) -> None:
211
209
  pass
212
210
 
213
211
 
214
- def add_coverage(test: Callable, operation: APIOperation, generation_modes: list[GenerationMode]) -> Callable:
215
- for example in _iter_coverage_cases(operation, generation_modes):
216
- adjust_urlencoded_payload(example)
217
- test = hypothesis.example(case=example)(test)
212
+ def add_coverage(
213
+ test: Callable,
214
+ operation: APIOperation,
215
+ generation_modes: list[GenerationMode],
216
+ auth_storage: AuthStorage | None,
217
+ as_strategy_kwargs: dict[str, Any],
218
+ ) -> Callable:
219
+ from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
220
+
221
+ auth_context = auths.AuthContext(
222
+ operation=operation,
223
+ app=operation.app,
224
+ )
225
+ overrides = {
226
+ container: as_strategy_kwargs[container]
227
+ for container in LOCATION_TO_CONTAINER.values()
228
+ if container in as_strategy_kwargs
229
+ }
230
+ for case in _iter_coverage_cases(operation, generation_modes):
231
+ if case.media_type and operation.schema.transport.get_first_matching_media_type(case.media_type) is None:
232
+ continue
233
+ adjust_urlencoded_payload(case)
234
+ auths.set_on_case(case, auth_context, auth_storage)
235
+ for container_name, value in overrides.items():
236
+ container = getattr(case, container_name)
237
+ if container is None:
238
+ setattr(case, container_name, value)
239
+ else:
240
+ container.update(value)
241
+
242
+ test = hypothesis.example(case=case)(test)
218
243
  return test
219
244
 
220
245
 
@@ -229,23 +254,123 @@ class Instant:
229
254
  return perf_counter() - self.start
230
255
 
231
256
 
257
+ class Template:
258
+ __slots__ = ("_components", "_template", "_serializers")
259
+
260
+ def __init__(self, serializers: dict[str, Callable]) -> None:
261
+ self._components: dict[ComponentKind, ComponentInfo] = {}
262
+ self._template: dict[str, Any] = {}
263
+ self._serializers = serializers
264
+
265
+ def __contains__(self, key: str) -> bool:
266
+ return key in self._template
267
+
268
+ def __getitem__(self, key: str) -> dict:
269
+ return self._template[key]
270
+
271
+ def get(self, key: str, default: Any = None) -> dict:
272
+ return self._template.get(key, default)
273
+
274
+ def add_parameter(self, location: str, name: str, value: coverage.GeneratedValue) -> None:
275
+ from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
276
+
277
+ component_name = LOCATION_TO_CONTAINER[location]
278
+ kind = ComponentKind(component_name)
279
+ info = self._components.get(kind)
280
+ if info is None:
281
+ self._components[kind] = ComponentInfo(mode=value.generation_mode)
282
+ elif value.generation_mode == GenerationMode.NEGATIVE:
283
+ info.mode = GenerationMode.NEGATIVE
284
+
285
+ container = self._template.setdefault(component_name, {})
286
+ container[name] = value.value
287
+
288
+ def set_body(self, body: coverage.GeneratedValue, media_type: str) -> None:
289
+ self._template["body"] = body.value
290
+ self._template["media_type"] = media_type
291
+ self._components[ComponentKind.BODY] = ComponentInfo(mode=body.generation_mode)
292
+
293
+ def _serialize(self, kwargs: dict[str, Any]) -> dict[str, Any]:
294
+ from schemathesis.specs.openapi._hypothesis import quote_all
295
+
296
+ output = {}
297
+ for container_name, value in kwargs.items():
298
+ serializer = self._serializers.get(container_name)
299
+ if container_name in ("headers", "cookies") and isinstance(value, dict):
300
+ value = _stringify_value(value, container_name)
301
+ if serializer is not None:
302
+ value = serializer(value)
303
+ if container_name == "query" and isinstance(value, dict):
304
+ value = _stringify_value(value, container_name)
305
+ if container_name == "path_parameters" and isinstance(value, dict):
306
+ value = _stringify_value(quote_all(value), container_name)
307
+ output[container_name] = value
308
+ return output
309
+
310
+ def unmodified(self) -> TemplateValue:
311
+ kwargs = self._template.copy()
312
+ kwargs = self._serialize(kwargs)
313
+ return TemplateValue(kwargs=kwargs, components=self._components.copy())
314
+
315
+ def with_body(self, *, media_type: str, value: coverage.GeneratedValue) -> TemplateValue:
316
+ kwargs = {**self._template, "media_type": media_type, "body": value.value}
317
+ kwargs = self._serialize(kwargs)
318
+ components = {**self._components, ComponentKind.BODY: ComponentInfo(mode=value.generation_mode)}
319
+ return TemplateValue(kwargs=kwargs, components=components)
320
+
321
+ def with_parameter(self, *, location: str, name: str, value: coverage.GeneratedValue) -> TemplateValue:
322
+ from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
323
+
324
+ container_name = LOCATION_TO_CONTAINER[location]
325
+ container = self._template[container_name]
326
+ return self.with_container(
327
+ container_name=container_name, value={**container, name: value.value}, generation_mode=value.generation_mode
328
+ )
329
+
330
+ def with_container(self, *, container_name: str, value: Any, generation_mode: GenerationMode) -> TemplateValue:
331
+ kwargs = {**self._template, container_name: value}
332
+ components = {**self._components, ComponentKind(container_name): ComponentInfo(mode=generation_mode)}
333
+ kwargs = self._serialize(kwargs)
334
+ return TemplateValue(kwargs=kwargs, components=components)
335
+
336
+
337
+ @dataclass
338
+ class TemplateValue:
339
+ kwargs: dict[str, Any]
340
+ components: dict[ComponentKind, ComponentInfo]
341
+ __slots__ = ("kwargs", "components")
342
+
343
+
344
+ def _stringify_value(val: Any, container_name: str) -> Any:
345
+ if val is None:
346
+ return "null"
347
+ if val is True:
348
+ return "true"
349
+ if val is False:
350
+ return "false"
351
+ if isinstance(val, (int, float)):
352
+ return str(val)
353
+ if isinstance(val, list):
354
+ if container_name == "query":
355
+ # Having a list here ensures there will be multiple query parameters wit the same name
356
+ return [_stringify_value(item, container_name) for item in val]
357
+ # use comma-separated values style for arrays
358
+ return ",".join(_stringify_value(sub, container_name) for sub in val)
359
+ if isinstance(val, dict):
360
+ return {key: _stringify_value(sub, container_name) for key, sub in val.items()}
361
+ return val
362
+
363
+
232
364
  def _iter_coverage_cases(
233
365
  operation: APIOperation, generation_modes: list[GenerationMode]
234
366
  ) -> Generator[Case, None, None]:
235
367
  from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
236
368
  from schemathesis.specs.openapi.examples import find_in_responses, find_matching_in_responses
237
-
238
- def _stringify_value(val: Any, location: str) -> str | list[str]:
239
- if isinstance(val, list):
240
- if location == "query":
241
- # Having a list here ensures there will be multiple query parameters wit the same name
242
- return [json.dumps(item) for item in val]
243
- # use comma-separated values style for arrays
244
- return ",".join(json.dumps(sub) for sub in val)
245
- return json.dumps(val)
369
+ from schemathesis.specs.openapi.serialization import get_serializers_for_operation
246
370
 
247
371
  generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
248
- template: dict[str, Any] = {}
372
+ serializers = get_serializers_for_operation(operation)
373
+ template = Template(serializers)
249
374
 
250
375
  instant = Instant()
251
376
  responses = find_in_responses(operation)
@@ -261,11 +386,7 @@ def _iter_coverage_cases(
261
386
  value = next(gen, NOT_SET)
262
387
  if isinstance(value, NotSet):
263
388
  continue
264
- container = template.setdefault(LOCATION_TO_CONTAINER[location], {})
265
- if location in ("header", "cookie", "path", "query") and not isinstance(value.value, str):
266
- container[name] = _stringify_value(value.value, location)
267
- else:
268
- container[name] = value.value
389
+ template.add_parameter(location, name, value)
269
390
  generators[(location, name)] = gen
270
391
  template_time = instant.elapsed
271
392
  if operation.body:
@@ -286,16 +407,16 @@ def _iter_coverage_cases(
286
407
  elapsed = instant.elapsed
287
408
  if "body" not in template:
288
409
  template_time += elapsed
289
- template["body"] = value.value
290
- template["media_type"] = body.media_type
410
+ template.set_body(value, body.media_type)
411
+ data = template.with_body(value=value, media_type=body.media_type)
291
412
  yield operation.Case(
292
- **{**template, "body": value.value, "media_type": body.media_type},
413
+ **data.kwargs,
293
414
  meta=CaseMetadata(
294
415
  generation=GenerationInfo(
295
416
  time=elapsed,
296
417
  mode=value.generation_mode,
297
418
  ),
298
- components={},
419
+ components=data.components,
299
420
  phase=PhaseInfo.coverage(
300
421
  description=value.description,
301
422
  location=value.location,
@@ -309,14 +430,15 @@ def _iter_coverage_cases(
309
430
  instant = Instant()
310
431
  try:
311
432
  next_value = next(iterator)
433
+ data = template.with_body(value=next_value, media_type=body.media_type)
312
434
  yield operation.Case(
313
- **{**template, "body": next_value.value, "media_type": body.media_type},
435
+ **data.kwargs,
314
436
  meta=CaseMetadata(
315
437
  generation=GenerationInfo(
316
438
  time=instant.elapsed,
317
439
  mode=value.generation_mode,
318
440
  ),
319
- components={},
441
+ components=data.components,
320
442
  phase=PhaseInfo.coverage(
321
443
  description=next_value.description,
322
444
  location=next_value.location,
@@ -328,37 +450,34 @@ def _iter_coverage_cases(
328
450
  except StopIteration:
329
451
  break
330
452
  elif GenerationMode.POSITIVE in generation_modes:
453
+ data = template.unmodified()
331
454
  yield operation.Case(
332
- **template,
455
+ **data.kwargs,
333
456
  meta=CaseMetadata(
334
457
  generation=GenerationInfo(
335
458
  time=template_time,
336
459
  mode=GenerationMode.POSITIVE,
337
460
  ),
338
- components={},
461
+ components=data.components,
339
462
  phase=PhaseInfo.coverage(description="Default positive test case"),
340
463
  ),
341
464
  )
342
465
 
343
466
  for (location, name), gen in generators.items():
344
- container_name = LOCATION_TO_CONTAINER[location]
345
- container = template[container_name]
346
467
  iterator = iter(gen)
347
468
  while True:
348
469
  instant = Instant()
349
470
  try:
350
471
  value = next(iterator)
351
- if location in ("header", "cookie", "path", "query") and not isinstance(value.value, str):
352
- generated = _stringify_value(value.value, location)
353
- else:
354
- generated = value.value
472
+ data = template.with_parameter(location=location, name=name, value=value)
355
473
  except StopIteration:
356
474
  break
475
+
357
476
  yield operation.Case(
358
- **{**template, container_name: {**container, name: generated}},
477
+ **data.kwargs,
359
478
  meta=CaseMetadata(
360
479
  generation=GenerationInfo(time=instant.elapsed, mode=value.generation_mode),
361
- components={},
480
+ components=data.components,
362
481
  phase=PhaseInfo.coverage(
363
482
  description=value.description,
364
483
  location=value.location,
@@ -372,12 +491,13 @@ def _iter_coverage_cases(
372
491
  methods = {"get", "put", "post", "delete", "options", "patch", "trace"} - set(operation.schema[operation.path])
373
492
  for method in sorted(methods):
374
493
  instant = Instant()
494
+ data = template.unmodified()
375
495
  yield operation.Case(
376
- **template,
496
+ **data.kwargs,
377
497
  method=method.upper(),
378
498
  meta=CaseMetadata(
379
499
  generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
380
- components={},
500
+ components=data.components,
381
501
  phase=PhaseInfo.coverage(description=f"Unspecified HTTP method: {method.upper()}"),
382
502
  ),
383
503
  )
@@ -390,11 +510,16 @@ def _iter_coverage_cases(
390
510
  # I.e. contains just `default` value without any other keywords
391
511
  value = container.get(parameter.name, NOT_SET)
392
512
  if value is not NOT_SET:
513
+ data = template.with_container(
514
+ container_name="query",
515
+ value={**container, parameter.name: [value, value]},
516
+ generation_mode=GenerationMode.NEGATIVE,
517
+ )
393
518
  yield operation.Case(
394
- **{**template, "query": {**container, parameter.name: [value, value]}},
519
+ **data.kwargs,
395
520
  meta=CaseMetadata(
396
521
  generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
397
- components={},
522
+ components=data.components,
398
523
  phase=PhaseInfo.coverage(
399
524
  description=f"Duplicate `{parameter.name}` query parameter",
400
525
  parameter=parameter.name,
@@ -410,11 +535,16 @@ def _iter_coverage_cases(
410
535
  location = parameter.location
411
536
  container_name = LOCATION_TO_CONTAINER[location]
412
537
  container = template[container_name]
538
+ data = template.with_container(
539
+ container_name=container_name,
540
+ value={k: v for k, v in container.items() if k != name},
541
+ generation_mode=GenerationMode.NEGATIVE,
542
+ )
413
543
  yield operation.Case(
414
- **{**template, container_name: {k: v for k, v in container.items() if k != name}},
544
+ **data.kwargs,
415
545
  meta=CaseMetadata(
416
546
  generation=GenerationInfo(time=instant.elapsed, mode=GenerationMode.NEGATIVE),
417
- components={},
547
+ components=data.components,
418
548
  phase=PhaseInfo.coverage(
419
549
  description=f"Missing `{name}` at {location}",
420
550
  parameter=name,
@@ -449,22 +579,17 @@ def _iter_coverage_cases(
449
579
  _generation_mode: GenerationMode,
450
580
  _instant: Instant,
451
581
  ) -> Case:
452
- if _location in ("header", "cookie", "path", "query"):
453
- container = {
454
- name: _stringify_value(val, _location) if not isinstance(val, str) else val
455
- for name, val in container_values.items()
456
- }
457
- else:
458
- container = container_values
459
-
582
+ data = template.with_container(
583
+ container_name=_container_name, value=container_values, generation_mode=_generation_mode
584
+ )
460
585
  return operation.Case(
461
- **{**template, _container_name: container},
586
+ **data.kwargs,
462
587
  meta=CaseMetadata(
463
588
  generation=GenerationInfo(
464
589
  time=_instant.elapsed,
465
590
  mode=_generation_mode,
466
591
  ),
467
- components={},
592
+ components=data.components,
468
593
  phase=PhaseInfo.coverage(
469
594
  description=description,
470
595
  parameter=_parameter,
@@ -68,6 +68,52 @@ class ExtractedParam:
68
68
  __slots__ = ("definition", "value")
69
69
 
70
70
 
71
+ @dataclass
72
+ class ExtractionFailure:
73
+ """Represents a failure to extract data from a transition."""
74
+
75
+ # e.g., "GetUser"
76
+ id: str
77
+ case_id: str
78
+ # e.g., "POST /users"
79
+ source: str
80
+ # e.g., "GET /users/{userId}"
81
+ target: str
82
+ # e.g., "userId"
83
+ parameter_name: str
84
+ # e.g., "$response.body#/id"
85
+ expression: str
86
+ # Previous test cases in the chain, from newest to oldest
87
+ # Stored as a case + response pair
88
+ history: list[tuple[Case, Response]]
89
+ # The actual response that caused the failure
90
+ response: Response
91
+ error: Exception | None
92
+
93
+ __slots__ = ("id", "case_id", "source", "target", "parameter_name", "expression", "history", "response", "error")
94
+
95
+ def __eq__(self, other: object) -> bool:
96
+ assert isinstance(other, ExtractionFailure)
97
+ return (
98
+ self.source == other.source
99
+ and self.target == other.target
100
+ and self.id == other.id
101
+ and self.parameter_name == other.parameter_name
102
+ and self.expression == other.expression
103
+ )
104
+
105
+ def __hash__(self) -> int:
106
+ return hash(
107
+ (
108
+ self.source,
109
+ self.target,
110
+ self.id,
111
+ self.parameter_name,
112
+ self.expression,
113
+ )
114
+ )
115
+
116
+
71
117
  @dataclass
72
118
  class StepOutput:
73
119
  """Output from a single transition of a state machine."""
@@ -172,7 +218,7 @@ class APIStateMachine(RuleBasedStateMachine):
172
218
  kwargs = self.get_call_kwargs(input.case)
173
219
  response = self.call(input.case, **kwargs)
174
220
  self.after_call(response, input.case)
175
- self.validate_response(response, input.case)
221
+ self.validate_response(response, input.case, **kwargs)
176
222
  return StepOutput(response, input.case)
177
223
 
178
224
  def before_call(self, case: Case) -> None:
@@ -266,7 +312,7 @@ class APIStateMachine(RuleBasedStateMachine):
266
312
  return {}
267
313
 
268
314
  def validate_response(
269
- self, response: Response, case: Case, additional_checks: list[CheckFunction] | None = None
315
+ self, response: Response, case: Case, additional_checks: list[CheckFunction] | None = None, **kwargs: Any
270
316
  ) -> None:
271
317
  """Validate an API response.
272
318
 
@@ -298,4 +344,4 @@ class APIStateMachine(RuleBasedStateMachine):
298
344
  all provided checks rather than only the first encountered exception.
299
345
  """
300
346
  __tracebackhide__ = True
301
- case.validate_response(response, additional_checks=additional_checks)
347
+ case.validate_response(response, additional_checks=additional_checks, transport_kwargs=kwargs)
@@ -14,7 +14,9 @@ if TYPE_CHECKING:
14
14
  @dataclass
15
15
  class NegativeDataRejectionConfig:
16
16
  # 5xx will pass through
17
- allowed_statuses: list[str] = field(default_factory=lambda: ["400", "401", "403", "404", "422", "428", "5xx"])
17
+ allowed_statuses: list[str] = field(
18
+ default_factory=lambda: ["400", "401", "403", "404", "406", "422", "428", "5xx"]
19
+ )
18
20
 
19
21
 
20
22
  @dataclass
@@ -10,9 +10,10 @@ from hypothesis.core import HypothesisHandle
10
10
  from pytest_subtests import SubTests
11
11
 
12
12
  from schemathesis.core.errors import InvalidSchema
13
- from schemathesis.core.result import Ok
13
+ from schemathesis.core.result import Ok, Result
14
14
  from schemathesis.filters import FilterSet, FilterValue, MatcherFunc, RegexValue, is_deprecated
15
- from schemathesis.generation.hypothesis.builder import get_all_tests
15
+ from schemathesis.generation import GenerationConfig
16
+ from schemathesis.generation.hypothesis.builder import HypothesisTestConfig, HypothesisTestMode, create_test
16
17
  from schemathesis.generation.hypothesis.given import (
17
18
  GivenArgsMark,
18
19
  GivenInput,
@@ -27,11 +28,48 @@ from schemathesis.pytest.control_flow import fail_on_no_matches
27
28
  from schemathesis.schemas import BaseSchema
28
29
 
29
30
  if TYPE_CHECKING:
31
+ import hypothesis
30
32
  from _pytest.fixtures import FixtureRequest
31
33
 
32
34
  from schemathesis.schemas import APIOperation
33
35
 
34
36
 
37
+ def get_all_tests(
38
+ *,
39
+ schema: BaseSchema,
40
+ test_func: Callable,
41
+ generation_config: GenerationConfig,
42
+ modes: list[HypothesisTestMode],
43
+ settings: hypothesis.settings | None = None,
44
+ seed: int | None = None,
45
+ as_strategy_kwargs: Callable[[APIOperation], dict[str, Any]] | None = None,
46
+ given_kwargs: dict[str, GivenInput] | None = None,
47
+ ) -> Generator[Result[tuple[APIOperation, Callable], InvalidSchema], None, None]:
48
+ """Generate all operations and Hypothesis tests for them."""
49
+ for result in schema.get_all_operations(generation_config=generation_config):
50
+ if isinstance(result, Ok):
51
+ operation = result.ok()
52
+ if callable(as_strategy_kwargs):
53
+ _as_strategy_kwargs = as_strategy_kwargs(operation)
54
+ else:
55
+ _as_strategy_kwargs = {}
56
+ test = create_test(
57
+ operation=operation,
58
+ test_func=test_func,
59
+ config=HypothesisTestConfig(
60
+ settings=settings,
61
+ modes=modes,
62
+ seed=seed,
63
+ generation=generation_config,
64
+ as_strategy_kwargs=_as_strategy_kwargs,
65
+ given_kwargs=given_kwargs or {},
66
+ ),
67
+ )
68
+ yield Ok((operation, test))
69
+ else:
70
+ yield result
71
+
72
+
35
73
  @dataclass
36
74
  class LazySchema:
37
75
  fixture_name: str
@@ -155,6 +193,7 @@ class LazySchema:
155
193
  schema=schema,
156
194
  test_func=test_func,
157
195
  settings=settings,
196
+ modes=list(HypothesisTestMode),
158
197
  generation_config=schema.generation_config,
159
198
  as_strategy_kwargs=as_strategy_kwargs,
160
199
  given_kwargs=given_kwargs,
@@ -168,7 +207,7 @@ class LazySchema:
168
207
  for result in tests:
169
208
  if isinstance(result, Ok):
170
209
  operation, sub_test = result.ok()
171
- subtests.item._nodeid = f"{node_id}[{operation.method.upper()} {operation.full_path}]"
210
+ subtests.item._nodeid = f"{node_id}[{operation.method.upper()} {operation.path}]"
172
211
  run_subtest(operation, fixtures, sub_test, subtests)
173
212
  else:
174
213
  _schema_error(subtests, result.err(), node_id)
@@ -236,8 +275,7 @@ SEPARATOR = "\n===================="
236
275
  def _schema_error(subtests: SubTests, error: InvalidSchema, node_id: str) -> None:
237
276
  """Run a failing test, that will show the underlying problem."""
238
277
  sub_test = error.as_failing_test_function()
239
- # `full_path` is always available in this case
240
- kwargs = {"path": error.full_path}
278
+ kwargs = {"path": error.path}
241
279
  if error.method:
242
280
  kwargs["method"] = error.method.upper()
243
281
  subtests.item._nodeid = _get_partial_node_name(node_id, **kwargs)