pytest-httpchain 0.7.0__tar.gz → 0.8.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.7.0 → pytest_httpchain-0.8.0}/PKG-INFO +14 -4
- {pytest_httpchain-0.7.0 → pytest_httpchain-0.8.0}/README.md +12 -2
- {pytest_httpchain-0.7.0 → pytest_httpchain-0.8.0}/pyproject.toml +2 -2
- {pytest_httpchain-0.7.0 → pytest_httpchain-0.8.0}/src/pytest_httpchain/carrier.py +18 -4
- {pytest_httpchain-0.7.0 → pytest_httpchain-0.8.0}/src/pytest_httpchain/cli.py +8 -27
- {pytest_httpchain-0.7.0 → pytest_httpchain-0.8.0}/src/pytest_httpchain/plugin.py +14 -7
- {pytest_httpchain-0.7.0 → pytest_httpchain-0.8.0}/src/pytest_httpchain/validation.py +72 -2
- {pytest_httpchain-0.7.0 → pytest_httpchain-0.8.0}/LICENSE +0 -0
- {pytest_httpchain-0.7.0 → pytest_httpchain-0.8.0}/src/pytest_httpchain/__init__.py +0 -0
- {pytest_httpchain-0.7.0 → pytest_httpchain-0.8.0}/src/pytest_httpchain/constants.py +0 -0
- {pytest_httpchain-0.7.0 → pytest_httpchain-0.8.0}/src/pytest_httpchain/dataflow.py +0 -0
- {pytest_httpchain-0.7.0 → pytest_httpchain-0.8.0}/src/pytest_httpchain/exceptions.py +0 -0
- {pytest_httpchain-0.7.0 → pytest_httpchain-0.8.0}/src/pytest_httpchain/har_writer.py +0 -0
- {pytest_httpchain-0.7.0 → pytest_httpchain-0.8.0}/src/pytest_httpchain/report_formatter.py +0 -0
- {pytest_httpchain-0.7.0 → pytest_httpchain-0.8.0}/src/pytest_httpchain/schema.py +0 -0
- {pytest_httpchain-0.7.0 → pytest_httpchain-0.8.0}/src/pytest_httpchain/utils.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-httpchain
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: pytest plugin for HTTP testing using JSON files
|
|
5
|
-
Keywords: testing,pytest,
|
|
5
|
+
Keywords: testing,pytest,httpx,http,api,integration-testing
|
|
6
6
|
Author: Alexander Eresov
|
|
7
7
|
Author-email: Alexander Eresov <aeresov@gmail.com>
|
|
8
8
|
License-Expression: MIT
|
|
@@ -197,11 +197,21 @@ pip install 'git+https://github.com/aeresov/pytest-httpchain@main'
|
|
|
197
197
|
|
|
198
198
|
- Test file discovery is based on this name pattern: `test_<name>.<suffix>.json`.
|
|
199
199
|
The `suffix` is configurable as pytest ini option, default value is **http**.
|
|
200
|
-
- `$ref` instructions can point to other files
|
|
200
|
+
- `$ref` instructions can point to other files using relative paths; absolute paths are rejected for security.
|
|
201
201
|
You can limit the depth of relative path traversal using `ref_parent_traversal_depth` ini option, default value is **3**.
|
|
202
202
|
- Template expressions support list/dict comprehensions. You can limit the maximum comprehension length using `max_comprehension_length` ini option, default value is **50000**.
|
|
203
203
|
- Parallel stage iterations (repeat/foreach) have a safety limit configurable via `max_parallel_iterations` ini option, default value is **10000**.
|
|
204
204
|
|
|
205
|
+
### HAR export
|
|
206
|
+
|
|
207
|
+
Pass `--output-dir DIR` on the pytest command line to write an [HAR](http://www.softwareishard.com/blog/har-12-spec/) file (and a "HAR File" report section) capturing each test's HTTP traffic:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
pytest --output-dir ./har-output
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
HAR files contain full requests/responses **including credential headers and saved tokens** — nothing is redacted, so scrub them before sharing. See the [HAR export docs](https://aeresov.github.io/pytest-httpchain/getting-started/#har-export).
|
|
214
|
+
|
|
205
215
|
## AI agent support
|
|
206
216
|
|
|
207
217
|
`pytest-httpchain` ships a scenario validator to help AI coding agents (and humans) author and check test scenarios.
|
|
@@ -241,7 +251,7 @@ A JSON Schema is published for as-you-type validation and autocomplete. Referenc
|
|
|
241
251
|
The hosted schema tracks the latest release; to pin the schema matching your installed version (e.g. for CI), emit it locally:
|
|
242
252
|
|
|
243
253
|
```bash
|
|
244
|
-
uvx pytest-httpchain schema
|
|
254
|
+
uvx pytest-httpchain schema > scenario.schema.json
|
|
245
255
|
```
|
|
246
256
|
|
|
247
257
|
### Inspecting scenarios
|
|
@@ -164,11 +164,21 @@ pip install 'git+https://github.com/aeresov/pytest-httpchain@main'
|
|
|
164
164
|
|
|
165
165
|
- Test file discovery is based on this name pattern: `test_<name>.<suffix>.json`.
|
|
166
166
|
The `suffix` is configurable as pytest ini option, default value is **http**.
|
|
167
|
-
- `$ref` instructions can point to other files
|
|
167
|
+
- `$ref` instructions can point to other files using relative paths; absolute paths are rejected for security.
|
|
168
168
|
You can limit the depth of relative path traversal using `ref_parent_traversal_depth` ini option, default value is **3**.
|
|
169
169
|
- Template expressions support list/dict comprehensions. You can limit the maximum comprehension length using `max_comprehension_length` ini option, default value is **50000**.
|
|
170
170
|
- Parallel stage iterations (repeat/foreach) have a safety limit configurable via `max_parallel_iterations` ini option, default value is **10000**.
|
|
171
171
|
|
|
172
|
+
### HAR export
|
|
173
|
+
|
|
174
|
+
Pass `--output-dir DIR` on the pytest command line to write an [HAR](http://www.softwareishard.com/blog/har-12-spec/) file (and a "HAR File" report section) capturing each test's HTTP traffic:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
pytest --output-dir ./har-output
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
HAR files contain full requests/responses **including credential headers and saved tokens** — nothing is redacted, so scrub them before sharing. See the [HAR export docs](https://aeresov.github.io/pytest-httpchain/getting-started/#har-export).
|
|
181
|
+
|
|
172
182
|
## AI agent support
|
|
173
183
|
|
|
174
184
|
`pytest-httpchain` ships a scenario validator to help AI coding agents (and humans) author and check test scenarios.
|
|
@@ -208,7 +218,7 @@ A JSON Schema is published for as-you-type validation and autocomplete. Referenc
|
|
|
208
218
|
The hosted schema tracks the latest release; to pin the schema matching your installed version (e.g. for CI), emit it locally:
|
|
209
219
|
|
|
210
220
|
```bash
|
|
211
|
-
uvx pytest-httpchain schema
|
|
221
|
+
uvx pytest-httpchain schema > scenario.schema.json
|
|
212
222
|
```
|
|
213
223
|
|
|
214
224
|
### Inspecting scenarios
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pytest-httpchain"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.0"
|
|
4
4
|
description = "pytest plugin for HTTP testing using JSON files"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -21,7 +21,7 @@ dependencies = [
|
|
|
21
21
|
"simpleeval>=1.0.3",
|
|
22
22
|
"typer>=0.16.0",
|
|
23
23
|
]
|
|
24
|
-
keywords = ["testing", "pytest", "
|
|
24
|
+
keywords = ["testing", "pytest", "httpx", "http", "api", "integration-testing"]
|
|
25
25
|
license = "MIT"
|
|
26
26
|
license-files = ["LICENSE"]
|
|
27
27
|
classifiers = [
|
|
@@ -155,9 +155,10 @@ class Carrier:
|
|
|
155
155
|
|
|
156
156
|
total = len(iteration_substitutions)
|
|
157
157
|
if total == 0:
|
|
158
|
-
#
|
|
159
|
-
# repeat is PositiveInt), but
|
|
160
|
-
#
|
|
158
|
+
# The models reject the static empty cases (foreach/combinations
|
|
159
|
+
# have min_length=1, repeat is PositiveInt), but a template- or
|
|
160
|
+
# $ref-sourced parallel config can still resolve to empty at
|
|
161
|
+
# runtime, so guard rather than silently send zero requests.
|
|
161
162
|
raise StageExecutionError("Parallel configuration produced zero iterations; foreach/repeat must yield at least one item")
|
|
162
163
|
if total > cls.max_parallel_iterations:
|
|
163
164
|
raise StageExecutionError(
|
|
@@ -543,6 +544,19 @@ class Carrier:
|
|
|
543
544
|
cls.client = None
|
|
544
545
|
|
|
545
546
|
|
|
547
|
+
def _normalize_cert(cert: Any) -> str | tuple[str, ...]:
|
|
548
|
+
"""Stringify SSL client-cert paths for httpx.
|
|
549
|
+
|
|
550
|
+
The model stores ``cert`` as ``pathlib.Path`` (single) or a tuple of Paths.
|
|
551
|
+
httpx builds the SSL context via ``load_cert_chain(*cert)`` for a non-tuple
|
|
552
|
+
cert, so a bare ``Path`` is unpacked and raises ``TypeError``. Passing string
|
|
553
|
+
paths avoids that for both the single-path and (cert, key) tuple forms.
|
|
554
|
+
"""
|
|
555
|
+
if isinstance(cert, list | tuple):
|
|
556
|
+
return tuple(str(p) for p in cert)
|
|
557
|
+
return str(cert)
|
|
558
|
+
|
|
559
|
+
|
|
546
560
|
def create_test_class(scenario: Scenario, class_name: str, max_parallel_iterations: int = 10_000) -> type[Carrier]:
|
|
547
561
|
"""Create a dynamic test class from a scenario definition."""
|
|
548
562
|
scenario_context = process_substitutions(scenario.substitutions)
|
|
@@ -553,7 +567,7 @@ def create_test_class(scenario: Scenario, class_name: str, max_parallel_iteratio
|
|
|
553
567
|
"http2": True,
|
|
554
568
|
}
|
|
555
569
|
if scenario.ssl.cert is not None:
|
|
556
|
-
client_kwargs["cert"] = resolved_ssl.cert
|
|
570
|
+
client_kwargs["cert"] = _normalize_cert(resolved_ssl.cert)
|
|
557
571
|
if scenario.auth:
|
|
558
572
|
resolved_auth = walk(scenario.auth, scenario_context)
|
|
559
573
|
auth_result = call_user_function(resolved_auth)
|
|
@@ -28,23 +28,6 @@ class GraphDirection(enum.StrEnum):
|
|
|
28
28
|
RefParentTraversalDepth = Annotated[int, typer.Option(help="Maximum $ref parent directory traversal depth.")]
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
def _emit(text: str, output: Path | None, label: str) -> None:
|
|
32
|
-
"""Write text to a file (with a confirmation) or echo it to stdout.
|
|
33
|
-
|
|
34
|
-
A failed write reports a clean ``error: ...`` and exits non-zero, matching
|
|
35
|
-
the load-error handling elsewhere in this module instead of a raw traceback.
|
|
36
|
-
"""
|
|
37
|
-
if output is None:
|
|
38
|
-
typer.echo(text)
|
|
39
|
-
return
|
|
40
|
-
try:
|
|
41
|
-
output.write_text(text + "\n")
|
|
42
|
-
except OSError as e:
|
|
43
|
-
typer.echo(f"error: cannot write {output}: {e}", err=True)
|
|
44
|
-
raise typer.Exit(1) from e
|
|
45
|
-
typer.echo(f"Wrote {label} to {output}")
|
|
46
|
-
|
|
47
|
-
|
|
48
31
|
@app.callback()
|
|
49
32
|
def main() -> None:
|
|
50
33
|
"""pytest-httpchain command-line tools."""
|
|
@@ -99,24 +82,23 @@ def validate(
|
|
|
99
82
|
|
|
100
83
|
|
|
101
84
|
@app.command()
|
|
102
|
-
def schema(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
85
|
+
def schema() -> None:
|
|
86
|
+
"""Emit the JSON Schema for scenario files (editor autocomplete/validation).
|
|
87
|
+
|
|
88
|
+
Writes to stdout; redirect to a file (``pytest-httpchain schema > scenario.schema.json``).
|
|
89
|
+
"""
|
|
106
90
|
from pytest_httpchain.schema import build_schema
|
|
107
91
|
|
|
108
|
-
|
|
109
|
-
_emit(text, output, "schema")
|
|
92
|
+
typer.echo(json.dumps(build_schema(), indent=2, default=str))
|
|
110
93
|
|
|
111
94
|
|
|
112
95
|
@app.command()
|
|
113
96
|
def resolve(
|
|
114
97
|
scenario: Annotated[Path, typer.Argument(help="Scenario JSON file to resolve.")],
|
|
115
|
-
output: Annotated[Path | None, typer.Option("--output", "-o", help="Write resolved JSON to PATH instead of stdout.")] = None,
|
|
116
98
|
ref_parent_traversal_depth: RefParentTraversalDepth = 3,
|
|
117
99
|
root_path: Annotated[Path | None, typer.Option("--root-path", help="Directory that constrains $ref resolution (default: nearest tests/ ancestor).")] = None,
|
|
118
100
|
) -> None:
|
|
119
|
-
"""Resolve $ref/$include/$merge and print the merged scenario JSON."""
|
|
101
|
+
"""Resolve $ref/$include/$merge and print the merged scenario JSON to stdout."""
|
|
120
102
|
from pytest_httpchain_jsonref import ReferenceResolverError, load_json
|
|
121
103
|
|
|
122
104
|
from pytest_httpchain.validation import resolve_root_path
|
|
@@ -131,8 +113,7 @@ def resolve(
|
|
|
131
113
|
typer.echo(f"error: {e}", err=True)
|
|
132
114
|
raise typer.Exit(1) from e
|
|
133
115
|
|
|
134
|
-
|
|
135
|
-
_emit(text, output, "resolved scenario")
|
|
116
|
+
typer.echo(json.dumps(data, indent=2, default=str))
|
|
136
117
|
|
|
137
118
|
|
|
138
119
|
def _load_for_inspection(path: Path, depth: int, root_path: Path | None = None) -> tuple["Scenario", dict]:
|
|
@@ -165,26 +165,33 @@ def pytest_addoption(parser: pytest.Parser) -> None:
|
|
|
165
165
|
|
|
166
166
|
|
|
167
167
|
def pytest_configure(config: pytest.Config) -> None:
|
|
168
|
-
# Numeric options are registered with type="int",
|
|
169
|
-
#
|
|
170
|
-
#
|
|
171
|
-
#
|
|
168
|
+
# Numeric options are registered with type="int", but pytest performs the
|
|
169
|
+
# int() conversion with a bare int(value) that raises ValueError for a
|
|
170
|
+
# non-integer ini value — which pytest renders as an INTERNALERROR traceback.
|
|
171
|
+
# Wrap the read so a garbage value becomes a clean usage error; the range
|
|
172
|
+
# checks below likewise raise pytest.UsageError.
|
|
173
|
+
def _getint(name: str) -> int:
|
|
174
|
+
try:
|
|
175
|
+
return config.getini(name)
|
|
176
|
+
except ValueError as e:
|
|
177
|
+
raise pytest.UsageError(f"{name} must be an integer: {e}") from None
|
|
178
|
+
|
|
172
179
|
suffix = str(config.getini(ConfigOptions.SUFFIX))
|
|
173
180
|
if not re.match(r"^[a-zA-Z0-9_-]{1,32}$", suffix):
|
|
174
181
|
raise pytest.UsageError("suffix must contain only alphanumeric characters, underscores, hyphens, and be ≤32 chars")
|
|
175
182
|
|
|
176
|
-
ref_parent_traversal_depth =
|
|
183
|
+
ref_parent_traversal_depth = _getint(ConfigOptions.REF_PARENT_TRAVERSAL_DEPTH)
|
|
177
184
|
if ref_parent_traversal_depth < 0:
|
|
178
185
|
raise pytest.UsageError("ref_parent_traversal_depth must be non-negative")
|
|
179
186
|
|
|
180
|
-
max_comprehension_length =
|
|
187
|
+
max_comprehension_length = _getint(ConfigOptions.MAX_COMPREHENSION_LENGTH)
|
|
181
188
|
if max_comprehension_length < 1:
|
|
182
189
|
raise pytest.UsageError("max_comprehension_length must be a positive integer")
|
|
183
190
|
if max_comprehension_length > 1_000_000:
|
|
184
191
|
raise pytest.UsageError("max_comprehension_length must not exceed 1,000,000")
|
|
185
192
|
simpleeval.MAX_COMPREHENSION_LENGTH = max_comprehension_length # ty: ignore[invalid-assignment]
|
|
186
193
|
|
|
187
|
-
max_parallel_iterations =
|
|
194
|
+
max_parallel_iterations = _getint(ConfigOptions.MAX_PARALLEL_ITERATIONS)
|
|
188
195
|
if max_parallel_iterations < 1:
|
|
189
196
|
raise pytest.UsageError("max_parallel_iterations must be a positive integer")
|
|
190
197
|
if max_parallel_iterations > 1_000_000:
|
|
@@ -32,6 +32,9 @@ Code Severity Meaning
|
|
|
32
32
|
014 error Invalid JSON syntax
|
|
33
33
|
015 error Failed to parse JSON file
|
|
34
34
|
016 error Fixture referenced in a scenario-level template
|
|
35
|
+
017 error Scenario-level template references an undefined name
|
|
36
|
+
018 warning Verify expression is not a template (``{{ }}``) — asserts nothing
|
|
37
|
+
019 error Invalid pytest marker expression (scenario or stage ``marks``)
|
|
35
38
|
020 warning Referenced file does not exist (deep, opt-in)
|
|
36
39
|
021 warning Schema file is not valid JSON / not a valid schema (deep)
|
|
37
40
|
022 warning User function cannot be imported (deep)
|
|
@@ -64,7 +67,7 @@ from pytest_httpchain_models import (
|
|
|
64
67
|
Verify,
|
|
65
68
|
VerifyStep,
|
|
66
69
|
)
|
|
67
|
-
from pytest_httpchain_templates import TEMPLATE_BUILTINS, TEMPLATE_PATTERN
|
|
70
|
+
from pytest_httpchain_templates import TEMPLATE_BUILTINS, TEMPLATE_PATTERN, is_complete_template
|
|
68
71
|
|
|
69
72
|
Severity = Literal["error", "warning"]
|
|
70
73
|
|
|
@@ -90,6 +93,8 @@ class DiagnosticCode:
|
|
|
90
93
|
PARSE_ERROR = "HTTPCHAIN015"
|
|
91
94
|
FIXTURE_IN_SCENARIO_TEMPLATE = "HTTPCHAIN016"
|
|
92
95
|
SCENARIO_UNDEFINED_VAR = "HTTPCHAIN017"
|
|
96
|
+
NONTEMPLATE_EXPRESSION = "HTTPCHAIN018"
|
|
97
|
+
INVALID_MARKER = "HTTPCHAIN019"
|
|
93
98
|
# Deep (opt-in) checks: imports, signatures, referenced files.
|
|
94
99
|
REFERENCED_FILE_NOT_FOUND = "HTTPCHAIN020"
|
|
95
100
|
SCHEMA_FILE_INVALID = "HTTPCHAIN021"
|
|
@@ -453,6 +458,21 @@ def _verify_diagnostics(scenario: Scenario) -> list[Diagnostic]:
|
|
|
453
458
|
)
|
|
454
459
|
)
|
|
455
460
|
|
|
461
|
+
# A verify expression is meant to be a complete ``{{ }}`` template that
|
|
462
|
+
# evaluates to a truthy/falsy value. A plain string (e.g. a forgotten
|
|
463
|
+
# ``{{ }}``) is non-empty and therefore always truthy at runtime, so the
|
|
464
|
+
# assertion silently passes — it tests nothing.
|
|
465
|
+
for expr in verify.expressions:
|
|
466
|
+
if isinstance(expr, str) and not is_complete_template(expr):
|
|
467
|
+
diagnostics.append(
|
|
468
|
+
_diag(
|
|
469
|
+
DiagnosticCode.NONTEMPLATE_EXPRESSION,
|
|
470
|
+
"warning",
|
|
471
|
+
f"Stage '{stage.name}': verify expression {expr!r} is not a template ({{{{ }}}}); it is always truthy and asserts nothing",
|
|
472
|
+
location=location,
|
|
473
|
+
)
|
|
474
|
+
)
|
|
475
|
+
|
|
456
476
|
# Overlap is compared on the raw (unrendered) strings: identical
|
|
457
477
|
# entries — including identical templates — are caught. A contradiction
|
|
458
478
|
# that only emerges after rendering (e.g. a template that resolves to a
|
|
@@ -483,6 +503,47 @@ def _verify_diagnostics(scenario: Scenario) -> list[Diagnostic]:
|
|
|
483
503
|
return diagnostics
|
|
484
504
|
|
|
485
505
|
|
|
506
|
+
def _marker_diagnostics(scenario: Scenario) -> list[Diagnostic]:
|
|
507
|
+
"""Validate scenario- and stage-level pytest marker expressions.
|
|
508
|
+
|
|
509
|
+
Markers are parsed by ``make_marker`` only at collection time, so a malformed
|
|
510
|
+
marker (``skip(``) or an unsupported form (``foo.bar``) would pass ``validate``
|
|
511
|
+
yet crash collection. Run the same parser here so the validator catches it as
|
|
512
|
+
an error, keeping ``validate`` a faithful pre-flight check of what collection
|
|
513
|
+
will accept.
|
|
514
|
+
"""
|
|
515
|
+
import warnings
|
|
516
|
+
|
|
517
|
+
from pytest_httpchain.utils import make_marker
|
|
518
|
+
|
|
519
|
+
diagnostics: list[Diagnostic] = []
|
|
520
|
+
|
|
521
|
+
def _check(marks: list[str], location: str) -> None:
|
|
522
|
+
for mark in marks:
|
|
523
|
+
try:
|
|
524
|
+
# We only care whether the marker parses; constructing it would
|
|
525
|
+
# otherwise emit PytestUnknownMarkWarning for custom (unregistered)
|
|
526
|
+
# marks, which is noise during validation.
|
|
527
|
+
with warnings.catch_warnings():
|
|
528
|
+
warnings.simplefilter("ignore")
|
|
529
|
+
make_marker(mark)
|
|
530
|
+
except (ValueError, SyntaxError) as e:
|
|
531
|
+
diagnostics.append(
|
|
532
|
+
_diag(
|
|
533
|
+
DiagnosticCode.INVALID_MARKER,
|
|
534
|
+
"error",
|
|
535
|
+
f"Invalid marker {mark!r}: {e}",
|
|
536
|
+
location=location,
|
|
537
|
+
)
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
_check(scenario.marks, "marks")
|
|
541
|
+
for i, stage in enumerate(scenario.stages):
|
|
542
|
+
_check(stage.marks, f"stages[{i}].marks")
|
|
543
|
+
|
|
544
|
+
return diagnostics
|
|
545
|
+
|
|
546
|
+
|
|
486
547
|
# --------------------------------------------------------------------------- #
|
|
487
548
|
# Deep (opt-in) validation: imports, signatures, referenced files.
|
|
488
549
|
# These touch the filesystem and import user code, so they NEVER run at
|
|
@@ -829,6 +890,8 @@ def check_scenario(scenario: Scenario, test_data: dict[str, Any]) -> tuple[list[
|
|
|
829
890
|
|
|
830
891
|
diagnostics.extend(_verify_diagnostics(scenario))
|
|
831
892
|
|
|
893
|
+
diagnostics.extend(_marker_diagnostics(scenario))
|
|
894
|
+
|
|
832
895
|
scenario_info = ScenarioInfo(
|
|
833
896
|
num_stages=len(scenario.stages),
|
|
834
897
|
stage_names=stage_names,
|
|
@@ -919,7 +982,14 @@ def validate_scenario(
|
|
|
919
982
|
root_path=root_path,
|
|
920
983
|
)
|
|
921
984
|
except ReferenceResolverError as e:
|
|
922
|
-
|
|
985
|
+
# The resolver wraps a plain JSON syntax error as a ReferenceResolverError
|
|
986
|
+
# (chaining the JSONDecodeError as __cause__). Report those under the
|
|
987
|
+
# accurate "Invalid JSON syntax" code rather than the $ref-flavored one,
|
|
988
|
+
# which would mislead when no reference is involved.
|
|
989
|
+
if isinstance(e.__cause__, json.JSONDecodeError):
|
|
990
|
+
diagnostics.append(_diag(DiagnosticCode.INVALID_JSON, "error", f"Invalid JSON syntax: {e.__cause__}"))
|
|
991
|
+
else:
|
|
992
|
+
diagnostics.append(_diag(DiagnosticCode.REF_ERROR, "error", f"JSON reference resolution error: {e}"))
|
|
923
993
|
return _result(diagnostics)
|
|
924
994
|
except json.JSONDecodeError as e:
|
|
925
995
|
diagnostics.append(_diag(DiagnosticCode.INVALID_JSON, "error", f"Invalid JSON syntax: {e}"))
|
|
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
|