Coverage for tests / integration / test_prettier_convergence.py: 100%
44 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Integration test for prettier fmt convergence with proseWrap.
3Verifies that lintro's fix retry logic handles prettier's non-idempotent
4``proseWrap: "always"`` behavior on programmatically-generated markdown
5content (the pattern that triggers the lgtm-ci release workflow failure).
6"""
8from __future__ import annotations
10import tempfile
11from collections.abc import Callable, Generator
12from pathlib import Path
14import pytest
15from assertpy import assert_that
17from lintro.tools.definitions.prettier import PrettierPlugin
18from lintro.utils.tool_executor import _run_fix_with_retry
20# Markdown content that triggers prettier non-idempotency with proseWrap: always.
21# Long lines with inline links and mixed punctuation can cause prettier to
22# re-wrap differently on a second pass.
23PROBLEMATIC_CHANGELOG = """\
24# Changelog
26## [0.52.4](https://github.com/example/repo/compare/v0.52.3...v0.52.4) (2026-02-24)
28### Bug Fixes
30* **ci:** publish semver-tagged Docker images on release ([#637](https://github.com/example/repo/issues/637)) ([850de62](https://github.com/example/repo/commit/850de6200000000000000000000000000000dead))
31* **ci:** use full 40-char SHA for immutable Docker image tags to comply with SLSA provenance requirements and sigstore verification constraints ([#639](https://github.com/example/repo/issues/639)) ([1950030](https://github.com/example/repo/commit/195003000000000000000000000000000000dead))
32* **ruff:** pass incremental and tool_name to file discovery so that incremental mode and tool-specific file patterns are respected during ruff check and fix operations ([#629](https://github.com/example/repo/issues/629)) ([83ac42d](https://github.com/example/repo/commit/83ac42d00000000000000000000000000000dead))
33"""
36@pytest.fixture
37def changelog_project() -> Generator[Path, None, None]:
38 """Create a temp project with problematic CHANGELOG.md content.
40 Yields:
41 Path: Path to the temporary project directory.
42 """
43 with tempfile.TemporaryDirectory() as tmpdir:
44 project = Path(tmpdir)
45 (project / "CHANGELOG.md").write_text(PROBLEMATIC_CHANGELOG)
46 yield project
49@pytest.fixture
50def prettier_plugin(
51 skip_if_tool_unavailable: Callable[[str], None],
52 lintro_test_mode: str,
53) -> PrettierPlugin:
54 """Provide a PrettierPlugin instance, skipping if prettier unavailable.
56 Args:
57 skip_if_tool_unavailable: Fixture to skip if prettier is not installed.
58 lintro_test_mode: Fixture that sets LINTRO_TEST_MODE=1.
60 Returns:
61 A PrettierPlugin instance.
62 """
63 skip_if_tool_unavailable("prettier")
64 return PrettierPlugin()
67@pytest.mark.prettier
68def test_prettier_fmt_converges_on_changelog(
69 changelog_project: Path,
70 prettier_plugin: PrettierPlugin,
71) -> None:
72 """Verify that lintro fmt converges on non-idempotent markdown content.
74 This test reproduces the lgtm-ci release failure where prettier --write
75 followed by prettier --check reports remaining issues because
76 ``proseWrap: "always"`` re-wraps content differently on each pass.
78 The fix retry logic should make this converge within max_fix_retries.
80 Args:
81 changelog_project: Temp dir with problematic CHANGELOG.md.
82 prettier_plugin: PrettierPlugin instance.
83 """
84 changelog = str(changelog_project / "CHANGELOG.md")
86 # Run single pass first to establish baseline — may or may not converge
87 # depending on prettier version and content specifics
88 single_pass_result = _run_fix_with_retry(
89 tool=prettier_plugin,
90 paths=[changelog],
91 options={},
92 max_retries=1,
93 )
94 single_remaining = single_pass_result.remaining_issues_count or 0
96 # Reset file to original content for the multi-pass test
97 (changelog_project / "CHANGELOG.md").write_text(PROBLEMATIC_CHANGELOG)
99 retry_result = _run_fix_with_retry(
100 tool=prettier_plugin,
101 paths=[changelog],
102 options={},
103 max_retries=3,
104 )
106 # With retry, the result should converge (0 remaining issues)
107 retry_remaining = retry_result.remaining_issues_count or 0
108 assert_that(retry_remaining).is_equal_to(0)
109 assert_that(retry_result.success).is_true()
111 # Retry should do at least as well as single pass
112 assert_that(retry_remaining).is_less_than_or_equal_to(single_remaining)
114 # Verify the file is stable: running check again should find no issues
115 final_check = prettier_plugin.check([changelog], {})
116 assert_that(final_check.success).is_true()
117 assert_that(final_check.issues_count).is_equal_to(0)
120@pytest.mark.prettier
121def test_prettier_fmt_stable_after_convergence(
122 changelog_project: Path,
123 prettier_plugin: PrettierPlugin,
124) -> None:
125 """Verify that prettier output is stable after convergence.
127 After fix with retry converges, running fix again should be a no-op.
129 Args:
130 changelog_project: Temp dir with problematic CHANGELOG.md.
131 prettier_plugin: PrettierPlugin instance.
132 """
133 changelog = str(changelog_project / "CHANGELOG.md")
135 # Converge first
136 converge_result = _run_fix_with_retry(
137 tool=prettier_plugin,
138 paths=[changelog],
139 options={},
140 max_retries=3,
141 )
142 assert_that(converge_result.success).is_true()
144 # Capture content after convergence
145 converged_content = (changelog_project / "CHANGELOG.md").read_text()
147 # Run fix again — content should not change
148 second_result = prettier_plugin.fix([changelog], {})
149 after_second_content = (changelog_project / "CHANGELOG.md").read_text()
151 assert_that(after_second_content).is_equal_to(converged_content)
152 second_remaining = second_result.remaining_issues_count or 0
153 assert_that(second_remaining).is_equal_to(0)