Skip to content

Commit 6048625

Browse files
authored
Fix: enable inline-snapshot update by using assert instead of pytest.fail (#2571)
* fix: enable parallel testing for multiple Python versions in tox configuration * fix: improve file comparison logic in test assertions * fix: improve error handling for missing external files in assertions * feat: add generated models and enums for data handling * fix: enhance assertion error messaging for inline snapshot updates * fix: add unified diff formatting for content mismatch in assertions * fix: enable colored output for pytest in tox configuration * fix: enhance inline snapshot error messaging and formatting * fix: improve inline snapshot update messaging and formatting * fix: enable parallel execution for Python 3.12 dependencies in tox configuration * fix: add Api model and enable Live Execution flag in JobRun * fix: add FieldModel class with Pydantic validation * fix: update job naming in test configuration for clarity
1 parent 25d8685 commit 6048625

14 files changed

Lines changed: 276 additions & 39 deletions

File tree

.github/workflows/test.yaml

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ concurrency:
1616
jobs:
1717
test:
1818
name: >-
19-
${{ matrix.py || matrix.tox_env }} on
19+
${{ matrix.py || matrix.name }} on
2020
${{ matrix.os == 'windows-latest' && 'Windows' || (matrix.os == 'macos-latest' && 'macOS' || 'Ubuntu') }}
2121
strategy:
2222
fail-fast: false
@@ -25,13 +25,20 @@ jobs:
2525
os: [ubuntu-24.04, windows-latest, macos-latest]
2626
tox_env: ['']
2727
include:
28-
- tox_env: py3.12-black24
29-
- tox_env: py3.12-black23
30-
- tox_env: py3.12-black22
31-
- tox_env: py3.12-isort7
32-
- tox_env: py3.12-isort6
33-
- tox_env: py3.12-isort5
34-
- tox_env: py3.12-pydantic1
28+
- tox_env: py3.12-black24-parallel
29+
name: py3.12-black24
30+
- tox_env: py3.12-black23-parallel
31+
name: py3.12-black23
32+
- tox_env: py3.12-black22-parallel
33+
name: py3.12-black22
34+
- tox_env: py3.12-isort7-parallel
35+
name: py3.12-isort7
36+
- tox_env: py3.12-isort6-parallel
37+
name: py3.12-isort6
38+
- tox_env: py3.12-isort5-parallel
39+
name: py3.12-isort5
40+
- tox_env: py3.12-pydantic1-parallel
41+
name: py3.12-pydantic1
3542
runs-on: ${{ matrix.os == '' && 'ubuntu-24.04' || matrix.os }}
3643
env:
3744
OS: ${{ matrix.os == '' && 'ubuntu-24.04' || matrix.os}}
@@ -47,17 +54,19 @@ jobs:
4754
- name: Install tox
4855
run: uv tool install --python-preference only-managed --python 3.13 tox --with tox-uv
4956
- name: Setup Python test environment
50-
run: tox run -vv --notest --skip-missing-interpreters false -e ${{ matrix.py || matrix.tox_env }}
57+
run: tox run -vv --notest --skip-missing-interpreters false -e ${{ matrix.py && format('{0}-parallel', matrix.py) || matrix.tox_env }}
5158
env:
5259
UV_PYTHON_PREFERENCE: "only-managed"
5360
- name: Run test suite
54-
run: tox run --skip-uv-sync --skip-pkg-install -e ${{ matrix.py || matrix.tox_env }}
61+
run: tox run --skip-uv-sync --skip-pkg-install -e ${{ matrix.py && format('{0}-parallel', matrix.py) || matrix.tox_env }}
5562
env:
5663
UV_PYTHON_PREFERENCE: "only-managed"
5764
- name: Rename coverage report file
5865
run: |
5966
import os; import sys
60-
os.rename(f".tox/.coverage.${{ matrix.py || matrix.tox_env }}", f".tox/.coverage.${{ matrix.py || matrix.tox_env}}-${{ matrix.os }}")
67+
env_name = "${{ matrix.py && format('{0}-parallel', matrix.py) || matrix.tox_env }}"
68+
base_name = "${{ matrix.py || matrix.tox_env }}"
69+
os.rename(f".tox/.coverage.{env_name}", f".tox/.coverage.{base_name}-${{ matrix.os }}")
6170
shell: python
6271
- name: Upload coverage data
6372
uses: actions/upload-artifact@v4

tests/conftest.py

Lines changed: 116 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import difflib
56
import inspect
67
import sys
78
from typing import TYPE_CHECKING, Any, Protocol
@@ -52,18 +53,116 @@ def _normalize_line_endings(text: str) -> str:
5253
return text.replace("\r\n", "\n")
5354

5455

56+
def _get_tox_env() -> str: # pragma: no cover
57+
"""Get the current tox environment name from TOX_ENV_NAME or fallback.
58+
59+
Strips '-parallel' suffix since inline-snapshot requires -n0 (single process).
60+
"""
61+
import os # noqa: PLC0415
62+
63+
env = os.environ.get("TOX_ENV_NAME", "<version>")
64+
# Remove -parallel suffix since inline-snapshot needs single process mode
65+
return env.removesuffix("-parallel")
66+
67+
68+
def _format_snapshot_hint(action: str) -> str: # pragma: no cover
69+
"""Format a hint message for inline-snapshot commands with rich formatting."""
70+
from io import StringIO # noqa: PLC0415
71+
72+
from rich.console import Console # noqa: PLC0415
73+
from rich.text import Text # noqa: PLC0415
74+
75+
tox_env = _get_tox_env()
76+
command = f" tox run -e {tox_env} -- --inline-snapshot={action}"
77+
78+
description = "To update the expected file, run:" if action == "fix" else "To create the expected file, run:"
79+
80+
output = StringIO()
81+
console = Console(file=output, force_terminal=True, width=200, soft_wrap=False)
82+
83+
console.print(Text(description, style="default"))
84+
console.print(Text(command, style="bold cyan"))
85+
86+
return output.getvalue()
87+
88+
89+
def _format_new_content(content: str) -> str: # pragma: no cover
90+
"""Format new content (for create mode) with green color."""
91+
from io import StringIO # noqa: PLC0415
92+
93+
from rich.console import Console # noqa: PLC0415
94+
from rich.text import Text # noqa: PLC0415
95+
96+
output = StringIO()
97+
console = Console(file=output, force_terminal=True, width=200, soft_wrap=False)
98+
99+
for line in content.splitlines():
100+
console.print(Text(f"+{line}", style="green"))
101+
102+
return output.getvalue()
103+
104+
105+
def _format_diff(expected: str, actual: str, expected_path: Path) -> str: # pragma: no cover
106+
"""Format a unified diff between expected and actual content with colors."""
107+
from io import StringIO # noqa: PLC0415
108+
109+
from rich.console import Console # noqa: PLC0415
110+
from rich.text import Text # noqa: PLC0415
111+
112+
expected_lines = expected.splitlines(keepends=True)
113+
actual_lines = actual.splitlines(keepends=True)
114+
diff_lines = list(
115+
difflib.unified_diff(
116+
expected_lines,
117+
actual_lines,
118+
fromfile=str(expected_path),
119+
tofile="actual",
120+
)
121+
)
122+
123+
if not diff_lines:
124+
return ""
125+
126+
output = StringIO()
127+
console = Console(file=output, force_terminal=True, width=200, soft_wrap=False)
128+
129+
for line in diff_lines:
130+
line_stripped = line.rstrip("\n")
131+
# Skip header lines since file path is already in the error message
132+
if line.startswith(("---", "+++")):
133+
continue
134+
if line.startswith("@@"):
135+
console.print(Text(line_stripped, style="cyan"))
136+
elif line.startswith("-"):
137+
console.print(Text(line_stripped, style="red"))
138+
elif line.startswith("+"):
139+
console.print(Text(line_stripped, style="green"))
140+
else:
141+
# Use default to override pytest's red color for E lines
142+
console.print(Text(line_stripped, style="default"))
143+
144+
return output.getvalue()
145+
146+
55147
def _assert_with_external_file(content: str, expected_path: Path) -> None:
56148
"""Assert content matches external file, handling line endings."""
57149
__tracebackhide__ = True
58-
expected = external_file(expected_path)
150+
try:
151+
expected = external_file(expected_path)
152+
except FileNotFoundError: # pragma: no cover
153+
hint = _format_snapshot_hint("create")
154+
formatted_content = _format_new_content(content)
155+
msg = f"Expected file not found: {expected_path}\n{hint}\n{formatted_content}"
156+
raise AssertionError(msg) from None # pragma: no cover
59157
normalized_content = _normalize_line_endings(content)
60158
if isinstance(expected, str): # pragma: no branch
61-
if normalized_content != _normalize_line_endings(expected): # pragma: no cover
62-
pytest.fail(f"Content mismatch for {expected_path}\nExpected:\n{expected}\n\nActual:\n{content}")
159+
normalized_expected = _normalize_line_endings(expected)
160+
if normalized_content != normalized_expected: # pragma: no cover
161+
hint = _format_snapshot_hint("fix")
162+
diff = _format_diff(normalized_expected, normalized_content, expected_path)
163+
msg = f"Content mismatch for {expected_path}\n{hint}\n{diff}"
164+
raise AssertionError(msg) from None
63165
else:
64-
expected_value = expected._load_value() # pragma: no cover
65-
if _normalize_line_endings(expected_value) == normalized_content: # pragma: no cover
66-
return # pragma: no cover
67166
assert expected == normalized_content # pragma: no cover
68167

69168

@@ -171,9 +270,17 @@ def assert_directory_content(
171270
assert_directory_content(tmp_path / "model", EXPECTED_PATH / "main_modular")
172271
"""
173272
__tracebackhide__ = True
174-
for expected_path in expected_dir.rglob(pattern):
175-
relative_path = expected_path.relative_to(expected_dir)
176-
output_path = output_dir / relative_path
273+
output_files = {p.relative_to(output_dir) for p in output_dir.rglob(pattern)}
274+
expected_files = {p.relative_to(expected_dir) for p in expected_dir.rglob(pattern)}
275+
276+
# Check for extra expected files (output missing files that are expected)
277+
extra = expected_files - output_files
278+
assert not extra, f"Expected files not in output: {extra}"
279+
280+
# Compare all output files (including new ones not yet in expected)
281+
for output_path in output_dir.rglob(pattern):
282+
relative_path = output_path.relative_to(output_dir)
283+
expected_path = expected_dir / relative_path
177284
result = output_path.read_text(encoding=encoding)
178285
_assert_with_external_file(result, expected_path)
179286

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# generated by datamodel-codegen:
2+
# filename: duplicate_field_constraints
3+
# timestamp: 2019-07-26T00:00:00+00:00
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# generated by datamodel-codegen:
2+
# filename: duplicate_field_constraints
3+
# timestamp: 2019-07-26T00:00:00+00:00
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# generated by datamodel-codegen:
2+
# filename: modular_default_enum_member
3+
# timestamp: 1985-10-26T08:21:00+00:00
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# generated by datamodel-codegen:
2+
# filename: nested_bar/bar.json
3+
# timestamp: 1985-10-26T08:21:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from enum import Enum
8+
9+
from pydantic import BaseModel
10+
11+
12+
class NestedBar(BaseModel):
13+
pass
14+
15+
16+
class LogLevels(Enum):
17+
DEBUG = 'DEBUG'
18+
INFO = 'INFO'
19+
ERROR = 'ERROR'
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# generated by datamodel-codegen:
2+
# filename: root_one_of
3+
# timestamp: 2019-07-26T00:00:00+00:00
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# generated by datamodel-codegen:
2+
# filename: bar.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Optional
8+
9+
from pydantic import BaseModel, Field
10+
11+
12+
class JobRun(BaseModel):
13+
enabled: Optional[bool] = Field(False, description='If Live Execution is Enabled.')
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# generated by datamodel-codegen:
2+
# filename: foo.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import List, Optional
8+
9+
from pydantic import BaseModel, Field
10+
11+
12+
class JobRun(BaseModel):
13+
enabled: Optional[bool] = Field(False, description='If Live Execution is enabled')
14+
resources: Optional[List[str]] = Field(
15+
None, description='Resource full classname to register to extend any endpoints.'
16+
)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# generated by datamodel-codegen:
2+
# filename: union.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Optional, Union
8+
9+
from pydantic import BaseModel, Extra, Field
10+
11+
from . import bar, foo
12+
13+
14+
class ExecutionContext(BaseModel):
15+
class Config:
16+
extra = Extra.forbid
17+
18+
__root__: Union[foo.JobRun, bar.JobRun] = Field(
19+
..., description='Execution Configuration.'
20+
)
21+
22+
23+
class App(BaseModel):
24+
runtime: Optional[ExecutionContext] = Field(
25+
None, description='Execution Configuration.'
26+
)

0 commit comments

Comments
 (0)