schemathesis 3.35.5__py3-none-any.whl → 3.36.1__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.
@@ -245,6 +245,11 @@ def _iter_coverage_cases(
245
245
  if operation.body:
246
246
  for body in operation.body:
247
247
  schema = body.as_json_schema(operation)
248
+ # Definition could be a list for Open API 2.0
249
+ definition = body.definition if isinstance(body.definition, dict) else {}
250
+ examples = [example["value"] for example in definition.get("examples", {}).values() if "value" in example]
251
+ if examples:
252
+ schema.setdefault("examples", []).extend(examples)
248
253
  gen = coverage.cover_schema_iter(ctx, schema)
249
254
  value = next(gen, NOT_SET)
250
255
  if isinstance(value, NotSet):
schemathesis/auths.py CHANGED
@@ -450,6 +450,7 @@ class AuthStorage(Generic[Auth]):
450
450
  data: Auth | None = _provider_get(provider, case, context)
451
451
  if data is not None:
452
452
  provider.set(case, data, context)
453
+ case._has_explicit_auth = True
453
454
  break
454
455
 
455
456
 
schemathesis/checks.py CHANGED
@@ -15,11 +15,12 @@ from .specs.openapi.checks import (
15
15
  )
16
16
 
17
17
  if TYPE_CHECKING:
18
- from .models import Case, CheckFunction
18
+ from .internal.checks import CheckContext, CheckFunction
19
+ from .models import Case
19
20
  from .transports.responses import GenericResponse
20
21
 
21
22
 
22
- def not_a_server_error(response: GenericResponse, case: Case) -> bool | None:
23
+ def not_a_server_error(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
23
24
  """A check to verify that the response is not a server-side error."""
24
25
  from .specs.graphql.schemas import GraphQLCase
25
26
  from .specs.graphql.validation import validate_graphql_response
@@ -64,14 +65,16 @@ def register(check: CheckFunction) -> CheckFunction:
64
65
  .. code-block:: python
65
66
 
66
67
  @schemathesis.check
67
- def new_check(response, case):
68
+ def new_check(ctx, response, case):
68
69
  # some awesome assertions!
69
70
  ...
70
71
  """
71
72
  from . import cli
73
+ from .internal.checks import wrap_check
72
74
 
75
+ _check = wrap_check(check)
73
76
  global ALL_CHECKS
74
77
 
75
- ALL_CHECKS += (check,)
76
- cli.CHECKS_TYPE.choices += (check.__name__,) # type: ignore
78
+ ALL_CHECKS += (_check,)
79
+ cli.CHECKS_TYPE.choices += (_check.__name__,) # type: ignore
77
80
  return check
@@ -104,13 +104,6 @@ DEPRECATED_SHOW_ERROR_TRACEBACKS_OPTION_WARNING = (
104
104
  "Warning: Option `--show-errors-tracebacks` is deprecated and will be removed in Schemathesis 4.0. "
105
105
  "Use `--show-trace` instead"
106
106
  )
107
- DEPRECATED_CONTRIB_UNIQUE_DATA_OPTION_WARNING = (
108
- "The `--contrib-unique-data` CLI option and the corresponding `schemathesis.contrib.unique_data` hook "
109
- "are **DEPRECATED**. The concept of this feature does not fit the core principles of Hypothesis where "
110
- "strategies are configurable on a per-example basis but this feature implies uniqueness across examples. "
111
- "This leads to cryptic error messages about external state and flaky test runs, "
112
- "therefore it will be removed in Schemathesis 4.0"
113
- )
114
107
  CASSETTES_PATH_INVALID_USAGE_MESSAGE = "Can't use `--store-network-log` and `--cassette-path` simultaneously"
115
108
  COLOR_OPTIONS_INVALID_USAGE_MESSAGE = "Can't use `--no-color` and `--force-color` simultaneously"
116
109
  PHASES_INVALID_USAGE_MESSAGE = "Can't use `--hypothesis-phases` and `--hypothesis-no-phases` simultaneously"
@@ -439,7 +432,7 @@ REPORT_TO_SERVICE = ReportToService()
439
432
  "-A",
440
433
  type=click.Choice(["basic", "digest"], case_sensitive=False),
441
434
  default="basic",
442
- help="Specify the authentication method",
435
+ help="Specify the authentication method. For custom authentication methods, see our Authentication documentation: https://schemathesis.readthedocs.io/en/stable/auth.html#custom-auth",
443
436
  show_default=True,
444
437
  metavar="",
445
438
  )
@@ -949,9 +942,6 @@ def run(
949
942
  entry for health_check in hypothesis_suppress_health_check for entry in health_check.as_hypothesis()
950
943
  ]
951
944
 
952
- if contrib_unique_data:
953
- click.secho(DEPRECATED_CONTRIB_UNIQUE_DATA_OPTION_WARNING, fg="yellow")
954
-
955
945
  if show_errors_tracebacks:
956
946
  click.secho(DEPRECATED_SHOW_ERROR_TRACEBACKS_OPTION_WARNING, fg="yellow")
957
947
  show_trace = show_errors_tracebacks
@@ -1160,8 +1150,6 @@ def run(
1160
1150
  else:
1161
1151
  _fixups.install(fixups)
1162
1152
 
1163
- if contrib_unique_data:
1164
- contrib.unique_data.install()
1165
1153
  if contrib_openapi_formats_uuid:
1166
1154
  contrib.openapi.formats.uuid.install()
1167
1155
  if contrib_openapi_fill_missing_examples:
@@ -1197,6 +1185,7 @@ def run(
1197
1185
  seed=hypothesis_seed,
1198
1186
  exit_first=exit_first,
1199
1187
  max_failures=max_failures,
1188
+ unique_data=contrib_unique_data,
1200
1189
  dry_run=dry_run,
1201
1190
  store_interactions=cassette_path is not None,
1202
1191
  checks=selected_checks,
@@ -1321,6 +1310,7 @@ def into_event_stream(
1321
1310
  exit_first: bool,
1322
1311
  max_failures: int | None,
1323
1312
  rate_limit: str | None,
1313
+ unique_data: bool,
1324
1314
  dry_run: bool,
1325
1315
  store_interactions: bool,
1326
1316
  stateful: Stateful | None,
@@ -1364,6 +1354,7 @@ def into_event_stream(
1364
1354
  exit_first=exit_first,
1365
1355
  max_failures=max_failures,
1366
1356
  started_at=started_at,
1357
+ unique_data=unique_data,
1367
1358
  dry_run=dry_run,
1368
1359
  store_interactions=store_interactions,
1369
1360
  checks=checks,
@@ -13,8 +13,7 @@ if TYPE_CHECKING:
13
13
 
14
14
  def install() -> None:
15
15
  warnings.warn(
16
- "The `--contrib-unique-data` CLI option and the corresponding `schemathesis.contrib.unique_data` hook "
17
- "are **DEPRECATED**. The concept of this feature does not fit the core principles of Hypothesis where "
16
+ "The `schemathesis.contrib.unique_data` hook is **DEPRECATED**. The concept of this feature does not fit the core principles of Hypothesis where "
18
17
  "strategies are configurable on a per-example basis but this feature implies uniqueness across examples. "
19
18
  "This leads to cryptic error messages about external state and flaky test runs, "
20
19
  "therefore it will be removed in Schemathesis 4.0",
@@ -4,7 +4,8 @@ import json
4
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, TypeVar, cast
7
+ from itertools import combinations
8
+ from typing import Any, Generator, Iterator, TypeVar, cast
8
9
 
9
10
  import jsonschema
10
11
  from hypothesis import strategies as st
@@ -162,6 +163,7 @@ def cover_schema_iter(ctx: CoverageContext, schema: dict | bool) -> Generator[Ge
162
163
  schema = {}
163
164
  else:
164
165
  types = schema.get("type", [])
166
+ push_examples_to_properties(schema)
165
167
  if not isinstance(types, list):
166
168
  types = [types] # type: ignore[unreachable]
167
169
  if not types:
@@ -238,6 +240,8 @@ def _get_properties(schema: dict | bool) -> dict | bool:
238
240
  if isinstance(schema, dict):
239
241
  if "example" in schema:
240
242
  return {"const": schema["example"]}
243
+ if "default" in schema:
244
+ return {"const": schema["default"]}
241
245
  if schema.get("examples"):
242
246
  return {"enum": schema["examples"]}
243
247
  if schema.get("type") == "object":
@@ -262,18 +266,33 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
262
266
  """Generate positive string values."""
263
267
  # Boundary and near boundary values
264
268
  min_length = schema.get("minLength")
269
+ if min_length == 0:
270
+ min_length = None
265
271
  max_length = schema.get("maxLength")
266
272
  example = schema.get("example")
267
273
  examples = schema.get("examples")
268
- if example or examples:
274
+ default = schema.get("default")
275
+ if example or examples or default:
269
276
  if example:
270
277
  yield PositiveValue(example)
271
278
  if examples:
272
279
  for example in examples:
273
280
  yield PositiveValue(example)
281
+ if (
282
+ default
283
+ and not (example is not None and default == example)
284
+ and not (examples is not None and any(default == ex for ex in examples))
285
+ ):
286
+ yield PositiveValue(default)
274
287
  elif not min_length and not max_length:
275
288
  # Default positive value
276
289
  yield PositiveValue(ctx.generate_from_schema(schema))
290
+ elif "pattern" in schema:
291
+ # Without merging `maxLength` & `minLength` into a regex it is problematic
292
+ # to generate a valid value as the unredlying machinery will resort to filtering
293
+ # and it is unlikely that it will generate a string of that length
294
+ yield PositiveValue(ctx.generate_from_schema(schema))
295
+ return
277
296
 
278
297
  seen = set()
279
298
 
@@ -327,13 +346,20 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
327
346
  multiple_of = schema.get("multipleOf")
328
347
  example = schema.get("example")
329
348
  examples = schema.get("examples")
349
+ default = schema.get("default")
330
350
 
331
- if example or examples:
351
+ if example or examples or default:
332
352
  if example:
333
353
  yield PositiveValue(example)
334
354
  if examples:
335
355
  for example in examples:
336
356
  yield PositiveValue(example)
357
+ if (
358
+ default
359
+ and not (example is not None and default == example)
360
+ and not (examples is not None and any(default == ex for ex in examples))
361
+ ):
362
+ yield PositiveValue(default)
337
363
  elif not minimum and not maximum:
338
364
  # Default positive value
339
365
  yield PositiveValue(ctx.generate_from_schema(schema))
@@ -382,13 +408,20 @@ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Gener
382
408
  seen = set()
383
409
  example = schema.get("example")
384
410
  examples = schema.get("examples")
411
+ default = schema.get("default")
385
412
 
386
- if example or examples:
413
+ if example or examples or default:
387
414
  if example:
388
415
  yield PositiveValue(example)
389
416
  if examples:
390
417
  for example in examples:
391
418
  yield PositiveValue(example)
419
+ if (
420
+ default
421
+ and not (example is not None and default == example)
422
+ and not (examples is not None and any(default == ex for ex in examples))
423
+ ):
424
+ yield PositiveValue(default)
392
425
  else:
393
426
  yield PositiveValue(template)
394
427
  seen.add(len(template))
@@ -425,19 +458,41 @@ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Gener
425
458
  def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Generator[GeneratedValue, None, None]:
426
459
  example = schema.get("example")
427
460
  examples = schema.get("examples")
461
+ default = schema.get("default")
428
462
 
429
- if example or examples:
463
+ if example or examples or default:
430
464
  if example:
431
465
  yield PositiveValue(example)
432
466
  if examples:
433
467
  for example in examples:
434
468
  yield PositiveValue(example)
469
+ if (
470
+ default
471
+ and not (example is not None and default == example)
472
+ and not (examples is not None and any(default == ex for ex in examples))
473
+ ):
474
+ yield PositiveValue(default)
475
+
435
476
  else:
436
477
  yield PositiveValue(template)
437
- # Only required properties
478
+
438
479
  properties = schema.get("properties", {})
439
- if set(properties) != set(schema.get("required", {})):
440
- only_required = {k: v for k, v in template.items() if k in schema.get("required", [])}
480
+ required = set(schema.get("required", []))
481
+ optional = list(set(properties) - required)
482
+ optional.sort()
483
+
484
+ # Generate combinations with required properties and one optional property
485
+ for name in optional:
486
+ combo = {k: v for k, v in template.items() if k in required or k == name}
487
+ if combo != template:
488
+ yield PositiveValue(combo)
489
+ # Generate one combination for each size from 2 to N-1
490
+ for selection in select_combinations(optional):
491
+ combo = {k: v for k, v in template.items() if k in required or k in selection}
492
+ yield PositiveValue(combo)
493
+ # Generate only required properties
494
+ if set(properties) != required:
495
+ only_required = {k: v for k, v in template.items() if k in required}
441
496
  yield PositiveValue(only_required)
442
497
  seen = set()
443
498
  for name, sub_schema in properties.items():
@@ -450,6 +505,11 @@ def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Gene
450
505
  seen.clear()
451
506
 
452
507
 
508
+ def select_combinations(optional: list[str]) -> Iterator[tuple[str, ...]]:
509
+ for size in range(2, len(optional)):
510
+ yield next(combinations(optional, size))
511
+
512
+
453
513
  def _negative_enum(ctx: CoverageContext, value: list) -> Generator[GeneratedValue, None, None]:
454
514
  strategy = JSON_STRATEGY.filter(lambda x: x not in value)
455
515
  # The exact negative value is not important here
@@ -528,3 +588,16 @@ def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Gene
528
588
  value = ctx.generate_from(negative_strategy, cached=True)
529
589
  yield NegativeValue(value)
530
590
  seen.add(_to_hashable_key(value))
591
+
592
+
593
+ def push_examples_to_properties(schema: dict[str, Any]) -> None:
594
+ """Push examples from the top-level 'examples' field to the corresponding properties."""
595
+ if "examples" in schema and "properties" in schema:
596
+ properties = schema["properties"]
597
+ for example in schema["examples"]:
598
+ for prop, value in example.items():
599
+ if prop in properties:
600
+ if "examples" not in properties[prop]:
601
+ properties[prop]["examples"] = []
602
+ if value not in schema["properties"][prop]["examples"]:
603
+ properties[prop]["examples"].append(value)
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import warnings
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING, Callable, Optional
7
+
8
+ if TYPE_CHECKING:
9
+ from ..types import RawAuth
10
+ from ..models import Case
11
+ from ..transports.responses import GenericResponse
12
+ from requests.structures import CaseInsensitiveDict
13
+
14
+
15
+ CheckFunction = Callable[["CheckContext", "GenericResponse", "Case"], Optional[bool]]
16
+
17
+
18
+ @dataclass
19
+ class CheckContext:
20
+ """Context for Schemathesis checks.
21
+
22
+ Provides access to broader test execution data beyond individual test cases.
23
+ """
24
+
25
+ auth: RawAuth | None = None
26
+ headers: CaseInsensitiveDict | None = None
27
+
28
+
29
+ def wrap_check(check: Callable) -> CheckFunction:
30
+ """Make older checks compatible with the new signature."""
31
+ signature = inspect.signature(check)
32
+ parameters = len(signature.parameters)
33
+
34
+ if parameters == 3:
35
+ # New style check, return as is
36
+ return check
37
+
38
+ if parameters == 2:
39
+ # Old style check, wrap it
40
+ warnings.warn(
41
+ f"The check function '{check.__name__}' uses an outdated signature. "
42
+ "Please update it to accept 'ctx' as the first argument: "
43
+ "(ctx: CheckContext, response: GenericResponse, case: Case) -> Optional[bool]",
44
+ DeprecationWarning,
45
+ stacklevel=2,
46
+ )
47
+
48
+ def wrapper(_: CheckContext, response: GenericResponse, case: Case) -> Optional[bool]:
49
+ return check(response, case)
50
+
51
+ wrapper.__name__ = check.__name__
52
+
53
+ return wrapper
54
+
55
+ raise ValueError(f"Invalid check function signature. Expected 2 or 3 parameters, got {parameters}")
schemathesis/models.py CHANGED
@@ -17,7 +17,6 @@ from typing import (
17
17
  Generic,
18
18
  Iterator,
19
19
  NoReturn,
20
- Optional,
21
20
  Sequence,
22
21
  Type,
23
22
  TypeVar,
@@ -46,6 +45,7 @@ from .exceptions import (
46
45
  )
47
46
  from .generation import DataGenerationMethod, GenerationConfig, generate_random_case_id
48
47
  from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, dispatch
48
+ from .internal.checks import CheckContext
49
49
  from .internal.copy import fast_deepcopy
50
50
  from .internal.deprecation import deprecated_function, deprecated_property
51
51
  from .internal.output import prepare_response_payload
@@ -65,6 +65,7 @@ if TYPE_CHECKING:
65
65
 
66
66
  from .auths import AuthStorage
67
67
  from .failures import FailureContext
68
+ from .internal.checks import CheckFunction
68
69
  from .schemas import BaseSchema
69
70
  from .serializers import Serializer
70
71
  from .stateful import Stateful, StatefulTest
@@ -135,8 +136,7 @@ def prepare_request_data(kwargs: dict[str, Any]) -> PreparedRequestData:
135
136
  )
136
137
 
137
138
 
138
- @dataclass
139
- class TestPhase(Enum):
139
+ class TestPhase(str, Enum):
140
140
  __test__ = False
141
141
 
142
142
  EXPLICIT = "explicit"
@@ -183,6 +183,7 @@ class Case:
183
183
  # The way the case was generated (None for manually crafted ones)
184
184
  data_generation_method: DataGenerationMethod | None = None
185
185
  _auth: requests.auth.AuthBase | None = None
186
+ _has_explicit_auth: bool = False
186
187
 
187
188
  def __repr__(self) -> str:
188
189
  parts = [f"{self.__class__.__name__}("]
@@ -421,6 +422,7 @@ class Case:
421
422
  additional_checks: tuple[CheckFunction, ...] = (),
422
423
  excluded_checks: tuple[CheckFunction, ...] = (),
423
424
  code_sample_style: str | None = None,
425
+ headers: dict[str, Any] | None = None,
424
426
  ) -> None:
425
427
  """Validate application response.
426
428
 
@@ -434,17 +436,30 @@ class Case:
434
436
  :param code_sample_style: Controls the style of code samples for failure reproduction.
435
437
  """
436
438
  __tracebackhide__ = True
439
+ from requests.structures import CaseInsensitiveDict
440
+
437
441
  from .checks import ALL_CHECKS
442
+ from .internal.checks import wrap_check
438
443
  from .transports.responses import get_payload, get_reason
439
444
 
440
- checks = checks or ALL_CHECKS
445
+ if checks:
446
+ _checks = tuple(wrap_check(check) for check in checks)
447
+ else:
448
+ _checks = checks
449
+ if additional_checks:
450
+ _additional_checks = tuple(wrap_check(check) for check in additional_checks)
451
+ else:
452
+ _additional_checks = additional_checks
453
+
454
+ checks = _checks or ALL_CHECKS
441
455
  checks = tuple(check for check in checks if check not in excluded_checks)
442
- additional_checks = tuple(check for check in additional_checks if check not in excluded_checks)
456
+ additional_checks = tuple(check for check in _additional_checks if check not in excluded_checks)
443
457
  failed_checks = []
458
+ ctx = CheckContext(headers=CaseInsensitiveDict(headers) if headers else None)
444
459
  for check in chain(checks, additional_checks):
445
460
  copied_case = self.partial_deepcopy()
446
461
  try:
447
- check(response, copied_case)
462
+ check(ctx, response, copied_case)
448
463
  except AssertionError as exc:
449
464
  maybe_set_assertion_message(exc, check.__name__)
450
465
  failed_checks.append(exc)
@@ -514,7 +529,7 @@ class Case:
514
529
  ) -> requests.Response:
515
530
  __tracebackhide__ = True
516
531
  response = self.call(base_url, session, headers, **kwargs)
517
- self.validate_response(response, checks, code_sample_style=code_sample_style)
532
+ self.validate_response(response, checks, code_sample_style=code_sample_style, headers=headers)
518
533
  return response
519
534
 
520
535
  def _get_url(self, base_url: str | None) -> str:
@@ -547,6 +562,8 @@ class Case:
547
562
  body=fast_deepcopy(self.body),
548
563
  generation_time=self.generation_time,
549
564
  id=self.id,
565
+ _auth=self._auth,
566
+ _has_explicit_auth=self._has_explicit_auth,
550
567
  )
551
568
 
552
569
 
@@ -1233,6 +1250,3 @@ class TestResultSet:
1233
1250
  def add_warning(self, warning: str) -> None:
1234
1251
  """Add a new warning to the warnings list."""
1235
1252
  self.warnings.append(warning)
1236
-
1237
-
1238
- CheckFunction = Callable[["GenericResponse", Case], Optional[bool]]
@@ -347,6 +347,7 @@ def from_schema(
347
347
  exit_first: bool = False,
348
348
  max_failures: int | None = None,
349
349
  started_at: str | None = None,
350
+ unique_data: bool = False,
350
351
  dry_run: bool = False,
351
352
  store_interactions: bool = False,
352
353
  stateful: Stateful | None = None,
@@ -373,7 +374,6 @@ def from_schema(
373
374
  probe_config = probe_config or ProbeConfig()
374
375
 
375
376
  hypothesis_settings = hypothesis_settings or hypothesis.settings(deadline=DEFAULT_DEADLINE)
376
- generation_config = generation_config or GenerationConfig()
377
377
  request_config = RequestConfig(
378
378
  timeout=request_timeout,
379
379
  tls_verify=request_tls_verify,
@@ -405,6 +405,7 @@ def from_schema(
405
405
  exit_first=exit_first,
406
406
  max_failures=max_failures,
407
407
  started_at=started_at,
408
+ unique_data=unique_data,
408
409
  dry_run=dry_run,
409
410
  store_interactions=store_interactions,
410
411
  stateful=stateful,
@@ -430,6 +431,7 @@ def from_schema(
430
431
  exit_first=exit_first,
431
432
  max_failures=max_failures,
432
433
  started_at=started_at,
434
+ unique_data=unique_data,
433
435
  dry_run=dry_run,
434
436
  store_interactions=store_interactions,
435
437
  stateful=stateful,
@@ -455,6 +457,7 @@ def from_schema(
455
457
  exit_first=exit_first,
456
458
  max_failures=max_failures,
457
459
  started_at=started_at,
460
+ unique_data=unique_data,
458
461
  dry_run=dry_run,
459
462
  store_interactions=store_interactions,
460
463
  stateful=stateful,
@@ -481,6 +484,7 @@ def from_schema(
481
484
  exit_first=exit_first,
482
485
  max_failures=max_failures,
483
486
  started_at=started_at,
487
+ unique_data=unique_data,
484
488
  dry_run=dry_run,
485
489
  store_interactions=store_interactions,
486
490
  stateful=stateful,
@@ -506,6 +510,7 @@ def from_schema(
506
510
  exit_first=exit_first,
507
511
  max_failures=max_failures,
508
512
  started_at=started_at,
513
+ unique_data=unique_data,
509
514
  dry_run=dry_run,
510
515
  store_interactions=store_interactions,
511
516
  stateful=stateful,
@@ -530,6 +535,7 @@ def from_schema(
530
535
  exit_first=exit_first,
531
536
  max_failures=max_failures,
532
537
  started_at=started_at,
538
+ unique_data=unique_data,
533
539
  dry_run=dry_run,
534
540
  store_interactions=store_interactions,
535
541
  stateful=stateful,
@@ -1,29 +1,41 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING
4
5
 
6
+ from ...constants import NOT_SET
5
7
  from ...models import TestResult, TestResultSet
6
- from typing import TYPE_CHECKING
7
8
 
8
9
  if TYPE_CHECKING:
9
- from ...exceptions import OperationSchemaError
10
10
  import threading
11
11
 
12
+ from ...exceptions import OperationSchemaError
13
+ from ...models import Case
14
+ from ...types import NotSet, RawAuth
15
+
12
16
 
13
17
  @dataclass
14
18
  class RunnerContext:
15
19
  """Holds context shared for a test run."""
16
20
 
17
21
  data: TestResultSet
22
+ auth: RawAuth | None
18
23
  seed: int | None
19
24
  stop_event: threading.Event
25
+ unique_data: bool
26
+ outcome_cache: dict[int, BaseException | None]
20
27
 
21
- __slots__ = ("data", "seed", "stop_event")
28
+ __slots__ = ("data", "auth", "seed", "stop_event", "unique_data", "outcome_cache")
22
29
 
23
- def __init__(self, seed: int | None, stop_event: threading.Event) -> None:
30
+ def __init__(
31
+ self, *, seed: int | None, auth: RawAuth | None, stop_event: threading.Event, unique_data: bool
32
+ ) -> None:
24
33
  self.data = TestResultSet(seed=seed)
34
+ self.auth = auth
25
35
  self.seed = seed
26
36
  self.stop_event = stop_event
37
+ self.outcome_cache = {}
38
+ self.unique_data = unique_data
27
39
 
28
40
  @property
29
41
  def is_stopped(self) -> bool:
@@ -54,5 +66,11 @@ class RunnerContext:
54
66
  def add_warning(self, message: str) -> None:
55
67
  self.data.add_warning(message)
56
68
 
69
+ def cache_outcome(self, case: Case, outcome: BaseException | None) -> None:
70
+ self.outcome_cache[hash(case)] = outcome
71
+
72
+ def get_cached_outcome(self, case: Case) -> BaseException | None | NotSet:
73
+ return self.outcome_cache.get(hash(case), NOT_SET)
74
+
57
75
 
58
76
  ALL_NOT_FOUND_WARNING_MESSAGE = "All API responses have a 404 status code. Did you specify the proper API location?"