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

1"""Integration test for prettier fmt convergence with proseWrap. 

2 

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""" 

7 

8from __future__ import annotations 

9 

10import tempfile 

11from collections.abc import Callable, Generator 

12from pathlib import Path 

13 

14import pytest 

15from assertpy import assert_that 

16 

17from lintro.tools.definitions.prettier import PrettierPlugin 

18from lintro.utils.tool_executor import _run_fix_with_retry 

19 

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 

25 

26## [0.52.4](https://github.com/example/repo/compare/v0.52.3...v0.52.4) (2026-02-24) 

27 

28### Bug Fixes 

29 

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""" 

34 

35 

36@pytest.fixture 

37def changelog_project() -> Generator[Path, None, None]: 

38 """Create a temp project with problematic CHANGELOG.md content. 

39 

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 

47 

48 

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. 

55 

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. 

59 

60 Returns: 

61 A PrettierPlugin instance. 

62 """ 

63 skip_if_tool_unavailable("prettier") 

64 return PrettierPlugin() 

65 

66 

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. 

73 

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. 

77 

78 The fix retry logic should make this converge within max_fix_retries. 

79 

80 Args: 

81 changelog_project: Temp dir with problematic CHANGELOG.md. 

82 prettier_plugin: PrettierPlugin instance. 

83 """ 

84 changelog = str(changelog_project / "CHANGELOG.md") 

85 

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 

95 

96 # Reset file to original content for the multi-pass test 

97 (changelog_project / "CHANGELOG.md").write_text(PROBLEMATIC_CHANGELOG) 

98 

99 retry_result = _run_fix_with_retry( 

100 tool=prettier_plugin, 

101 paths=[changelog], 

102 options={}, 

103 max_retries=3, 

104 ) 

105 

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() 

110 

111 # Retry should do at least as well as single pass 

112 assert_that(retry_remaining).is_less_than_or_equal_to(single_remaining) 

113 

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) 

118 

119 

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. 

126 

127 After fix with retry converges, running fix again should be a no-op. 

128 

129 Args: 

130 changelog_project: Temp dir with problematic CHANGELOG.md. 

131 prettier_plugin: PrettierPlugin instance. 

132 """ 

133 changelog = str(changelog_project / "CHANGELOG.md") 

134 

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() 

143 

144 # Capture content after convergence 

145 converged_content = (changelog_project / "CHANGELOG.md").read_text() 

146 

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() 

150 

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)