pytest-httpchain 0.5.0__tar.gz → 0.6.0__tar.gz
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.
- {pytest_httpchain-0.5.0 → pytest_httpchain-0.6.0}/PKG-INFO +1 -1
- {pytest_httpchain-0.5.0 → pytest_httpchain-0.6.0}/pyproject.toml +1 -1
- {pytest_httpchain-0.5.0 → pytest_httpchain-0.6.0}/src/pytest_httpchain/carrier.py +20 -5
- {pytest_httpchain-0.5.0 → pytest_httpchain-0.6.0}/src/pytest_httpchain/dataflow.py +17 -7
- {pytest_httpchain-0.5.0 → pytest_httpchain-0.6.0}/src/pytest_httpchain/schema.py +47 -0
- {pytest_httpchain-0.5.0 → pytest_httpchain-0.6.0}/src/pytest_httpchain/validation.py +69 -19
- {pytest_httpchain-0.5.0 → pytest_httpchain-0.6.0}/LICENSE +0 -0
- {pytest_httpchain-0.5.0 → pytest_httpchain-0.6.0}/README.md +0 -0
- {pytest_httpchain-0.5.0 → pytest_httpchain-0.6.0}/src/pytest_httpchain/__init__.py +0 -0
- {pytest_httpchain-0.5.0 → pytest_httpchain-0.6.0}/src/pytest_httpchain/cli.py +0 -0
- {pytest_httpchain-0.5.0 → pytest_httpchain-0.6.0}/src/pytest_httpchain/constants.py +0 -0
- {pytest_httpchain-0.5.0 → pytest_httpchain-0.6.0}/src/pytest_httpchain/exceptions.py +0 -0
- {pytest_httpchain-0.5.0 → pytest_httpchain-0.6.0}/src/pytest_httpchain/har_writer.py +0 -0
- {pytest_httpchain-0.5.0 → pytest_httpchain-0.6.0}/src/pytest_httpchain/plugin.py +0 -0
- {pytest_httpchain-0.5.0 → pytest_httpchain-0.6.0}/src/pytest_httpchain/report_formatter.py +0 -0
- {pytest_httpchain-0.5.0 → pytest_httpchain-0.6.0}/src/pytest_httpchain/utils.py +0 -0
|
@@ -79,12 +79,24 @@ class Carrier:
|
|
|
79
79
|
max_parallel_iterations: ClassVar[int] = 10_000
|
|
80
80
|
|
|
81
81
|
@classmethod
|
|
82
|
-
def
|
|
82
|
+
def _resolve_always_run(cls, stage: Stage, stage_fixtures: dict[str, Any]) -> bool:
|
|
83
|
+
"""Resolve ``always_run``, evaluating a template form against the context
|
|
84
|
+
available at stage start: fixtures and parametrize parameters plus the
|
|
85
|
+
global context (scenario substitutions and earlier saves). Stage
|
|
86
|
+
substitutions are not yet processed at this point. The result is coerced
|
|
87
|
+
with Python truthiness."""
|
|
88
|
+
if isinstance(stage.always_run, bool):
|
|
89
|
+
return stage.always_run
|
|
83
90
|
try:
|
|
84
|
-
|
|
85
|
-
|
|
91
|
+
return bool(walk(stage.always_run, ChainMap(stage_fixtures, cls.global_context)))
|
|
92
|
+
except TemplatesError as e:
|
|
93
|
+
raise StageExecutionError(f"Failed to evaluate always_run template: {e}") from e
|
|
86
94
|
|
|
87
|
-
|
|
95
|
+
@classmethod
|
|
96
|
+
def execute_stage(cls, stage: Stage, fixture_kwargs: dict[str, Any]) -> None:
|
|
97
|
+
try:
|
|
98
|
+
# prepare stage context (before the abort check, so an always_run
|
|
99
|
+
# template sees the same fixture layer as stage templates)
|
|
88
100
|
stage_fixtures: dict[str, Any] = {}
|
|
89
101
|
for name, value in fixture_kwargs.items():
|
|
90
102
|
if callable(value) and not inspect.isclass(value):
|
|
@@ -92,6 +104,9 @@ class Carrier:
|
|
|
92
104
|
else:
|
|
93
105
|
stage_fixtures[name] = value
|
|
94
106
|
|
|
107
|
+
if cls.aborted and not cls._resolve_always_run(stage, stage_fixtures):
|
|
108
|
+
pytest.skip(reason="Flow aborted")
|
|
109
|
+
|
|
95
110
|
# Build base context for iterations (substitutions + fixtures + global)
|
|
96
111
|
local_context = ChainMap(stage_fixtures, cls.global_context)
|
|
97
112
|
stage_substitutions = process_substitutions(stage.substitutions, local_context)
|
|
@@ -519,7 +534,7 @@ def create_test_class(scenario: Scenario, class_name: str, max_parallel_iteratio
|
|
|
519
534
|
parametrize_marker = pytest.mark.parametrize(",".join(param_names), param_values, ids=param_ids)
|
|
520
535
|
stage_method = parametrize_marker(stage_method)
|
|
521
536
|
|
|
522
|
-
all_fixtures = ["self"] + all_param_names + stage.fixtures
|
|
537
|
+
all_fixtures = ["self"] + list(dict.fromkeys(all_param_names + stage.fixtures + scenario.fixtures))
|
|
523
538
|
stage_method.__signature__ = inspect.Signature([inspect.Parameter(name, inspect.Parameter.POSITIONAL_OR_KEYWORD) for name in all_fixtures]) # type: ignore[assignment]
|
|
524
539
|
|
|
525
540
|
all_marks = [f"order({i})"] + stage.marks
|
|
@@ -14,6 +14,7 @@ from pydantic import BaseModel
|
|
|
14
14
|
from pytest_httpchain_models import Scenario
|
|
15
15
|
|
|
16
16
|
from pytest_httpchain.validation import (
|
|
17
|
+
_parameter_names,
|
|
17
18
|
extract_template_variables,
|
|
18
19
|
raw_stages,
|
|
19
20
|
resolve_root_path,
|
|
@@ -58,11 +59,15 @@ def analyze_dataflow(scenario: Scenario, test_data: dict[str, Any]) -> DataFlow:
|
|
|
58
59
|
|
|
59
60
|
A stage *consumes* a variable when its request/response/substitutions/parallel
|
|
60
61
|
templates reference a name saved by an earlier stage and not redefined locally
|
|
61
|
-
(own substitutions, parametrize/foreach parameters, or fixtures).
|
|
62
|
-
|
|
62
|
+
(own substitutions, parametrize/foreach parameters, or stage/scenario fixtures).
|
|
63
|
+
``always_run`` references count too, but only fixtures and parametrize
|
|
64
|
+
parameters shadow them — always_run resolves before stage substitutions exist.
|
|
65
|
+
``parametrize`` values are excluded — they resolve against scenario scope,
|
|
66
|
+
never saved values.
|
|
63
67
|
"""
|
|
64
68
|
raws = raw_stages(test_data)
|
|
65
69
|
saves_by_stage = [saved_in_stage(stage) for stage in scenario.stages]
|
|
70
|
+
scenario_fixture_names = set(scenario.fixtures)
|
|
66
71
|
|
|
67
72
|
first_save_stage: dict[str, int] = {}
|
|
68
73
|
for i, saved in enumerate(saves_by_stage):
|
|
@@ -80,9 +85,17 @@ def analyze_dataflow(scenario: Scenario, test_data: dict[str, Any]) -> DataFlow:
|
|
|
80
85
|
for key in ("request", "response", "substitutions", "parallel"):
|
|
81
86
|
extract_template_variables(raw.get(key), refs)
|
|
82
87
|
|
|
83
|
-
local
|
|
88
|
+
# Scenario fixtures count as local everywhere: at runtime they sit above
|
|
89
|
+
# the global context in the ChainMap, shadowing any same-named save.
|
|
90
|
+
local = stage_defined_names(stage) | scenario_fixture_names
|
|
84
91
|
consumes = {name for name in refs if name in cumulative_saves and name not in local}
|
|
85
92
|
|
|
93
|
+
# always_run resolves before stage substitutions exist, so only fixtures
|
|
94
|
+
# and parametrize parameters shadow an earlier save there.
|
|
95
|
+
always_run_refs = extract_template_variables(raw.get("always_run"))
|
|
96
|
+
always_run_local = set(stage.fixtures) | _parameter_names(stage.parametrize) | scenario_fixture_names
|
|
97
|
+
consumes |= {name for name in always_run_refs if name in cumulative_saves and name not in always_run_local}
|
|
98
|
+
|
|
86
99
|
by_producer: dict[int, list[str]] = {}
|
|
87
100
|
for name in consumes:
|
|
88
101
|
by_producer.setdefault(first_save_stage[name], []).append(name)
|
|
@@ -104,12 +117,9 @@ def analyze_dataflow(scenario: Scenario, test_data: dict[str, Any]) -> DataFlow:
|
|
|
104
117
|
|
|
105
118
|
cumulative_saves |= saves_by_stage[i]
|
|
106
119
|
|
|
107
|
-
scenario_fixtures = sorted(f for f in test_data["fixtures"] if isinstance(f, str)) if isinstance(test_data.get("fixtures"), list) else []
|
|
108
120
|
scenario_var_names = set(substitution_names(scenario.substitutions))
|
|
109
|
-
if isinstance(test_data.get("vars"), dict):
|
|
110
|
-
scenario_var_names |= {k for k in test_data["vars"] if isinstance(k, str)}
|
|
111
121
|
|
|
112
|
-
return DataFlow(stages=stages, edges=edges, scenario_fixtures=
|
|
122
|
+
return DataFlow(stages=stages, edges=edges, scenario_fixtures=sorted(scenario.fixtures), scenario_vars=sorted(scenario_var_names))
|
|
113
123
|
|
|
114
124
|
|
|
115
125
|
def load_scenario(path: Path, ref_parent_traversal_depth: int = 3) -> tuple[Scenario, dict[str, Any]]:
|
|
@@ -14,6 +14,18 @@ SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema"
|
|
|
14
14
|
SCHEMA_ID = "https://aeresov.github.io/pytest-httpchain/schema/scenario.schema.json"
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def _one_of_to_any_of(node: Any) -> None:
|
|
18
|
+
"""Recursively rename ``oneOf`` to ``anyOf`` in place."""
|
|
19
|
+
if isinstance(node, dict):
|
|
20
|
+
if "oneOf" in node:
|
|
21
|
+
node["anyOf"] = node.pop("oneOf")
|
|
22
|
+
for value in node.values():
|
|
23
|
+
_one_of_to_any_of(value)
|
|
24
|
+
elif isinstance(node, list):
|
|
25
|
+
for item in node:
|
|
26
|
+
_one_of_to_any_of(item)
|
|
27
|
+
|
|
28
|
+
|
|
17
29
|
def _add_jsonref_support(schema: dict[str, Any]) -> dict[str, Any]:
|
|
18
30
|
"""Allow ``$include``/``$merge``/``$ref`` objects as alternatives anywhere.
|
|
19
31
|
|
|
@@ -37,10 +49,28 @@ def _add_jsonref_support(schema: dict[str, Any]) -> dict[str, Any]:
|
|
|
37
49
|
"type": "string",
|
|
38
50
|
"description": "Alias for $include. Path to external JSON file, JSON pointer (#/path), or combined (file.json#/path).",
|
|
39
51
|
},
|
|
52
|
+
"$ref": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"description": "Legacy alias for $include. May conflict with VS Code's own $ref handling.",
|
|
55
|
+
},
|
|
40
56
|
},
|
|
57
|
+
# Without at least one directive key this branch would match EVERY
|
|
58
|
+
# object, silencing the strict alternative in the surrounding anyOf.
|
|
59
|
+
"anyOf": [
|
|
60
|
+
{"required": ["$include"]},
|
|
61
|
+
{"required": ["$merge"]},
|
|
62
|
+
{"required": ["$ref"]},
|
|
63
|
+
],
|
|
41
64
|
"additionalProperties": True,
|
|
42
65
|
}
|
|
43
66
|
|
|
67
|
+
# Pydantic emits oneOf for tagged unions. A reference object matches the
|
|
68
|
+
# JsonRef branch of EVERY union member (each $defs entry is wrapped below),
|
|
69
|
+
# which oneOf counts as "valid under more than one" and rejects. anyOf
|
|
70
|
+
# keeps the same accept set otherwise: members forbid each other's tag
|
|
71
|
+
# fields, so a non-reference object can never match two branches.
|
|
72
|
+
_one_of_to_any_of(schema)
|
|
73
|
+
|
|
44
74
|
for type_name, original_def in list(schema["$defs"].items()):
|
|
45
75
|
if type_name == "JsonRef":
|
|
46
76
|
continue
|
|
@@ -58,6 +88,23 @@ def _add_jsonref_support(schema: dict[str, Any]) -> dict[str, Any]:
|
|
|
58
88
|
if "description" in prop_def:
|
|
59
89
|
schema["properties"][prop_name]["description"] = prop_def.get("description")
|
|
60
90
|
|
|
91
|
+
# The Scenario model forbids extra keys, so the root carries
|
|
92
|
+
# additionalProperties: false. Keys that are legitimate in a scenario
|
|
93
|
+
# *file* but handled before model validation must be declared explicitly:
|
|
94
|
+
# "$schema" (editor metadata, stripped by the loader) and the reference
|
|
95
|
+
# directives (resolved by the loader, supported at the document root).
|
|
96
|
+
schema.setdefault("properties", {})
|
|
97
|
+
schema["properties"]["$schema"] = {
|
|
98
|
+
"type": "string",
|
|
99
|
+
"description": "URL of this schema, for editor as-you-type validation. Stripped before the file is parsed.",
|
|
100
|
+
}
|
|
101
|
+
for directive, directive_def in schema["$defs"]["JsonRef"]["properties"].items():
|
|
102
|
+
schema["properties"][directive] = directive_def
|
|
103
|
+
schema["properties"]["$ref"] = {
|
|
104
|
+
"type": "string",
|
|
105
|
+
"description": "Legacy alias for $include. May conflict with VS Code's own $ref handling.",
|
|
106
|
+
}
|
|
107
|
+
|
|
61
108
|
return schema
|
|
62
109
|
|
|
63
110
|
|
|
@@ -24,12 +24,14 @@ Code Severity Meaning
|
|
|
24
24
|
006 warning Verify step asserts nothing (no-op)
|
|
25
25
|
007 error Body ``contains``/``not_contains`` list the same substring
|
|
26
26
|
008 error Body ``matches``/``not_matches`` list the same pattern
|
|
27
|
+
009 warning Saved variable is shadowed by a scenario-level fixture
|
|
27
28
|
010 error File not found
|
|
28
29
|
011 error Path is not a file
|
|
29
30
|
012 error ``$ref`` resolution failed
|
|
30
31
|
013 warning File extension is not ``.json``
|
|
31
32
|
014 error Invalid JSON syntax
|
|
32
33
|
015 error Failed to parse JSON file
|
|
34
|
+
016 error Fixture referenced in a scenario-level template
|
|
33
35
|
020 warning Referenced file does not exist (deep, opt-in)
|
|
34
36
|
021 warning Schema file is not valid JSON / not a valid schema (deep)
|
|
35
37
|
022 warning User function cannot be imported (deep)
|
|
@@ -70,12 +72,14 @@ class DiagnosticCode:
|
|
|
70
72
|
NOOP_VERIFY = "HTTPCHAIN006"
|
|
71
73
|
CONTAINS_CONTRADICTION = "HTTPCHAIN007"
|
|
72
74
|
MATCHES_CONTRADICTION = "HTTPCHAIN008"
|
|
75
|
+
FIXTURE_SHADOWS_SAVE = "HTTPCHAIN009"
|
|
73
76
|
FILE_NOT_FOUND = "HTTPCHAIN010"
|
|
74
77
|
NOT_A_FILE = "HTTPCHAIN011"
|
|
75
78
|
REF_ERROR = "HTTPCHAIN012"
|
|
76
79
|
WRONG_EXTENSION = "HTTPCHAIN013"
|
|
77
80
|
INVALID_JSON = "HTTPCHAIN014"
|
|
78
81
|
PARSE_ERROR = "HTTPCHAIN015"
|
|
82
|
+
FIXTURE_IN_SCENARIO_TEMPLATE = "HTTPCHAIN016"
|
|
79
83
|
# Deep (opt-in) checks: imports, signatures, referenced files.
|
|
80
84
|
REFERENCED_FILE_NOT_FOUND = "HTTPCHAIN020"
|
|
81
85
|
SCHEMA_FILE_INVALID = "HTTPCHAIN021"
|
|
@@ -221,7 +225,7 @@ def stage_defined_names(stage: Stage) -> set[str]:
|
|
|
221
225
|
return names
|
|
222
226
|
|
|
223
227
|
|
|
224
|
-
def extract_defined_variables(scenario: Scenario
|
|
228
|
+
def extract_defined_variables(scenario: Scenario) -> set[str]:
|
|
225
229
|
"""Extract variable names made available before/within templates (scenario-wide).
|
|
226
230
|
|
|
227
231
|
Sources: ``vars`` and ``functions`` substitutions (scenario- and stage-level),
|
|
@@ -232,10 +236,6 @@ def extract_defined_variables(scenario: Scenario, test_data: dict[str, Any]) ->
|
|
|
232
236
|
"""
|
|
233
237
|
defined_vars: set[str] = set()
|
|
234
238
|
|
|
235
|
-
# Defensive: a top-level "vars" key is not a model field but is tolerated.
|
|
236
|
-
if isinstance(test_data.get("vars"), dict):
|
|
237
|
-
defined_vars.update(k for k in test_data["vars"] if isinstance(k, str))
|
|
238
|
-
|
|
239
239
|
defined_vars |= substitution_names(scenario.substitutions)
|
|
240
240
|
|
|
241
241
|
for stage in scenario.stages:
|
|
@@ -283,13 +283,16 @@ def _dataflow_diagnostics(scenario: Scenario, test_data: dict[str, Any]) -> list
|
|
|
283
283
|
Walks stages in execution order tracking which variables are available at
|
|
284
284
|
each reference site, mirroring the runtime scoping in ``carrier.py``:
|
|
285
285
|
|
|
286
|
-
* scenario-level substitutions
|
|
287
|
-
|
|
286
|
+
* scenario-level substitutions and scenario-level fixtures are available
|
|
287
|
+
everywhere;
|
|
288
288
|
* a stage's own substitutions, parametrize/foreach parameters and fixtures
|
|
289
289
|
are available to that stage's request and response;
|
|
290
290
|
* a value saved in a stage's response is available to that stage's response
|
|
291
291
|
and to *later* stages — but never to the same stage's request, and never
|
|
292
|
-
to earlier stages
|
|
292
|
+
to earlier stages;
|
|
293
|
+
* ``always_run`` is evaluated at stage start, before stage substitutions are
|
|
294
|
+
processed and before any iteration runs: only fixtures, parametrize
|
|
295
|
+
parameters, scenario substitutions and earlier saves are in scope.
|
|
293
296
|
|
|
294
297
|
A reference that is unavailable is reported as :data:`DiagnosticCode.FORWARD_REF`
|
|
295
298
|
if the name is saved somewhere later (an ordering bug) or
|
|
@@ -309,14 +312,9 @@ def _dataflow_diagnostics(scenario: Scenario, test_data: dict[str, Any]) -> list
|
|
|
309
312
|
# fixtures, no stage substitutions, no parameter names, no saved values exist
|
|
310
313
|
# yet. ``scenario_scope`` is that narrow set; ``scenario_available`` is the
|
|
311
314
|
# everywhere-available set used for ordinary request/response references.
|
|
312
|
-
scenario_scope: set[str] = set()
|
|
313
|
-
if isinstance(test_data.get("vars"), dict):
|
|
314
|
-
scenario_scope |= {k for k in test_data["vars"] if isinstance(k, str)}
|
|
315
|
-
scenario_scope |= substitution_names(scenario.substitutions)
|
|
315
|
+
scenario_scope: set[str] = set(substitution_names(scenario.substitutions))
|
|
316
316
|
|
|
317
|
-
scenario_available = set(
|
|
318
|
-
if isinstance(test_data.get("fixtures"), list):
|
|
319
|
-
scenario_available |= {f for f in test_data["fixtures"] if isinstance(f, str)}
|
|
317
|
+
scenario_available = scenario_scope | set(scenario.fixtures)
|
|
320
318
|
|
|
321
319
|
raws = raw_stages(test_data)
|
|
322
320
|
cumulative_saves: set[str] = set()
|
|
@@ -347,6 +345,31 @@ def _dataflow_diagnostics(scenario: Scenario, test_data: dict[str, Any]) -> list
|
|
|
347
345
|
)
|
|
348
346
|
)
|
|
349
347
|
|
|
348
|
+
# ``always_run`` resolves at stage start (carrier.execute_stage) against
|
|
349
|
+
# fixtures + parametrize parameters + global context — stage substitutions
|
|
350
|
+
# and foreach parameters don't exist yet, and neither do this stage's saves.
|
|
351
|
+
always_run_available = scenario_available | set(stage.fixtures) | _parameter_names(stage.parametrize) | cumulative_saves
|
|
352
|
+
for name in sorted(extract_template_variables(raw.get("always_run"))):
|
|
353
|
+
if name in always_run_available:
|
|
354
|
+
continue
|
|
355
|
+
if name in all_saved:
|
|
356
|
+
j = first_save_stage[name]
|
|
357
|
+
if j == i:
|
|
358
|
+
msg = f"Stage '{stage.name}': always_run references '{name}', which is only saved in this stage's response — always_run is evaluated before the stage runs"
|
|
359
|
+
else:
|
|
360
|
+
msg = f"Stage '{stage.name}': always_run references '{name}' before it is saved (saved in stage '{scenario.stages[j].name}')"
|
|
361
|
+
diagnostics.append(_diag(DiagnosticCode.FORWARD_REF, "warning", msg, location=stage.name))
|
|
362
|
+
else:
|
|
363
|
+
diagnostics.append(
|
|
364
|
+
_diag(
|
|
365
|
+
DiagnosticCode.UNDEFINED_VAR,
|
|
366
|
+
"warning",
|
|
367
|
+
f"Stage '{stage.name}': always_run references '{name}' — potentially not in scope; only fixtures, "
|
|
368
|
+
f"parametrize parameters, scenario substitutions, and variables saved by earlier stages are available",
|
|
369
|
+
location=stage.name,
|
|
370
|
+
)
|
|
371
|
+
)
|
|
372
|
+
|
|
350
373
|
undefined_here: set[str] = set()
|
|
351
374
|
|
|
352
375
|
for refs, available, in_request in (
|
|
@@ -642,14 +665,12 @@ def check_scenario(scenario: Scenario, test_data: dict[str, Any]) -> tuple[list[
|
|
|
642
665
|
)
|
|
643
666
|
)
|
|
644
667
|
|
|
645
|
-
fixtures: list[str] =
|
|
646
|
-
if isinstance(test_data.get("fixtures"), list):
|
|
647
|
-
fixtures.extend(test_data["fixtures"])
|
|
668
|
+
fixtures: list[str] = list(scenario.fixtures)
|
|
648
669
|
for stage in scenario.stages:
|
|
649
670
|
fixtures.extend(stage.fixtures)
|
|
650
671
|
fixtures = sorted(set(fixtures))
|
|
651
672
|
|
|
652
|
-
vars_defined = extract_defined_variables(scenario
|
|
673
|
+
vars_defined = extract_defined_variables(scenario)
|
|
653
674
|
vars_saved = extract_saved_variables(scenario)
|
|
654
675
|
vars_referenced = extract_template_variables(test_data)
|
|
655
676
|
|
|
@@ -663,6 +684,35 @@ def check_scenario(scenario: Scenario, test_data: dict[str, Any]) -> tuple[list[
|
|
|
663
684
|
)
|
|
664
685
|
)
|
|
665
686
|
|
|
687
|
+
# Scenario fixtures are injected into every stage and sit above the global
|
|
688
|
+
# context in the runtime ChainMap, so a save under the same name can never
|
|
689
|
+
# be read back — the fixture value always wins.
|
|
690
|
+
shadowed_saves = set(scenario.fixtures) & vars_saved
|
|
691
|
+
if shadowed_saves:
|
|
692
|
+
diagnostics.append(
|
|
693
|
+
_diag(
|
|
694
|
+
DiagnosticCode.FIXTURE_SHADOWS_SAVE,
|
|
695
|
+
"warning",
|
|
696
|
+
f"Saved variables shadowed by scenario-level fixtures: {sorted(shadowed_saves)} (fixture values win in every stage; these saves can never be read)",
|
|
697
|
+
)
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
# Scenario-level templates (substitutions/auth/ssl) resolve once at class
|
|
701
|
+
# creation (carrier.create_test_class), before any fixture exists — a fixture
|
|
702
|
+
# reference there is a guaranteed collection-time crash.
|
|
703
|
+
for key in ("substitutions", "auth", "ssl"):
|
|
704
|
+
scenario_level_refs = extract_template_variables(test_data.get(key))
|
|
705
|
+
fixture_refs = scenario_level_refs & set(fixtures)
|
|
706
|
+
if fixture_refs:
|
|
707
|
+
diagnostics.append(
|
|
708
|
+
_diag(
|
|
709
|
+
DiagnosticCode.FIXTURE_IN_SCENARIO_TEMPLATE,
|
|
710
|
+
"error",
|
|
711
|
+
f"Fixtures referenced in scenario-level '{key}' templates: {sorted(fixture_refs)} (scenario-level templates resolve at collection time, before fixtures exist)",
|
|
712
|
+
location=key,
|
|
713
|
+
)
|
|
714
|
+
)
|
|
715
|
+
|
|
666
716
|
# NOTE: response data (response/status_code/body/json/text/headers/cookies) is
|
|
667
717
|
# NOT ambient in {{ }} templates — it reaches save/verify handlers directly and
|
|
668
718
|
# only enters the template context via an earlier `save` step. So there are no
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|