Coverage for tests / integration / test_markdownlint_integration.py: 82%

79 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-04-03 18:53 +0000

1"""Integration tests for markdownlint tool.""" 

2 

3import shutil 

4import subprocess 

5from pathlib import Path 

6 

7import pytest 

8from assertpy import assert_that 

9from loguru import logger 

10 

11from lintro.parsers.markdownlint.markdownlint_issue import MarkdownlintIssue 

12from lintro.plugins import ToolRegistry 

13 

14logger.remove() 

15logger.add(lambda msg: print(msg, end=""), level="INFO") 

16SAMPLE_FILE = "test_samples/tools/config/markdown/markdownlint_violations.md" 

17 

18 

19def find_markdownlint_cmd() -> list[str] | None: 

20 """Find markdownlint-cli2 command. 

21 

22 Returns: 

23 Command list if found, None otherwise 

24 """ 

25 if shutil.which("npx"): 

26 return ["npx", "--yes", "markdownlint-cli2"] 

27 if shutil.which("markdownlint-cli2"): 

28 return ["markdownlint-cli2"] 

29 return None 

30 

31 

32def run_markdownlint_directly(file_path: Path) -> tuple[bool, str, int]: 

33 """Run markdownlint-cli2 directly on a file and return result tuple. 

34 

35 Args: 

36 file_path: Path to the file to check with markdownlint-cli2. 

37 

38 Returns: 

39 tuple[bool, str, int]: Success status, output text, and issue count. 

40 """ 

41 cmd_base = find_markdownlint_cmd() 

42 if cmd_base is None: 

43 pytest.skip("markdownlint-cli2 not found in PATH") 

44 # Use relative path from repo root to match lintro's behavior 

45 repo_root = Path(__file__).parent.parent.parent 

46 # Resolve to absolute path first if it's relative 

47 abs_file_path = file_path.resolve() if not file_path.is_absolute() else file_path 

48 try: 

49 relative_path = abs_file_path.relative_to(repo_root) 

50 except ValueError: 

51 # Fallback: use relative path calculation if not under repo root 

52 import os 

53 

54 relative_path = Path(os.path.relpath(abs_file_path, repo_root)) 

55 cmd = [*cmd_base, str(relative_path)] 

56 

57 result = subprocess.run( 

58 cmd, 

59 capture_output=True, 

60 text=True, 

61 check=False, 

62 cwd=repo_root, 

63 ) 

64 

65 # Count issues from output (non-empty lines are typically issues) 

66 issues = [ 

67 line 

68 for line in result.stdout.splitlines() 

69 if line.strip() and ":" in line and "MD" in line 

70 ] 

71 issues_count = len(issues) 

72 success = issues_count == 0 and result.returncode == 0 

73 return (success, result.stdout, issues_count) 

74 

75 

76@pytest.mark.markdownlint 

77def test_markdownlint_available() -> None: 

78 """Check if markdownlint-cli2 is available in PATH.""" 

79 cmd_base = find_markdownlint_cmd() 

80 if cmd_base is None: 

81 pytest.skip("markdownlint-cli2 not found in PATH") 

82 try: 

83 cmd = [*cmd_base, "--version"] 

84 result = subprocess.run( 

85 cmd, 

86 capture_output=True, 

87 text=True, 

88 check=False, 

89 timeout=10, 

90 ) 

91 assert_that(result.returncode).is_equal_to(0) 

92 except (subprocess.TimeoutExpired, FileNotFoundError): 

93 pytest.skip("markdownlint-cli2 not available") 

94 

95 

96@pytest.mark.markdownlint 

97def test_markdownlint_direct_vs_lintro_parity() -> None: 

98 """Compare direct markdownlint-cli2 output with lintro wrapper. 

99 

100 Runs markdownlint-cli2 directly on the sample file and compares the 

101 issue count with lintro's wrapper to ensure parity. 

102 """ 

103 sample_path = Path(SAMPLE_FILE) 

104 if not sample_path.exists(): 

105 pytest.skip(f"Sample file {SAMPLE_FILE} not found") 

106 

107 # Run markdownlint-cli2 directly 

108 direct_success, direct_output, direct_count = run_markdownlint_directly( 

109 sample_path, 

110 ) 

111 

112 # Run via lintro 

113 tool = ToolRegistry.get("markdownlint") 

114 assert_that(tool).is_not_none() 

115 # Clear exclude patterns to allow scanning test_samples 

116 tool.exclude_patterns = [] 

117 lintro_result = tool.check([str(sample_path)], {}) 

118 

119 # Compare issue counts (allow some variance due to parsing differences) 

120 # Direct count may include lines we don't parse, so lintro count <= direct 

121 assert_that(lintro_result.issues_count).is_greater_than_or_equal_to(0) 

122 # If direct found issues, lintro should find <= direct count (parsing may miss some) 

123 if direct_count > 0: 

124 assert_that(lintro_result.issues_count).is_less_than_or_equal_to(direct_count) 

125 assert_that(lintro_result.issues_count).is_greater_than(0) 

126 

127 # Both should agree on success/failure 

128 # Success is True when no issues found, False when issues are found 

129 assert_that(lintro_result.success).is_equal_to(direct_success) 

130 

131 

132@pytest.mark.markdownlint 

133def test_markdownlint_integration_basic() -> None: 

134 """Basic integration test for markdownlint tool. 

135 

136 Verifies that the tool can discover files, run checks, and parse output. 

137 """ 

138 sample_path = Path(SAMPLE_FILE) 

139 if not sample_path.exists(): 

140 pytest.skip(f"Sample file {SAMPLE_FILE} not found") 

141 

142 tool = ToolRegistry.get("markdownlint") 

143 assert_that(tool).is_not_none() 

144 result = tool.check([str(sample_path)], {}) 

145 

146 assert_that(result).is_not_none() 

147 assert_that(result.name).is_equal_to("markdownlint") 

148 assert_that(result.issues_count).is_greater_than_or_equal_to(0) 

149 

150 # If there are issues, verify they're properly structured 

151 if result.issues: 

152 issue = result.issues[0] 

153 # Use isinstance check for type narrowing 

154 if not isinstance(issue, MarkdownlintIssue): 

155 pytest.fail("issue should be MarkdownlintIssue") 

156 assert_that(issue.file).is_not_empty() 

157 assert_that(issue.line).is_greater_than(0) 

158 assert_that(issue.code).matches(r"^MD\d+$") 

159 assert_that(issue.message).is_not_empty()