Coverage for tests\test_lmcat.py: 94%

192 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2024-12-31 20:31 -0700

1import sys 

2import os 

3import shutil 

4import subprocess 

5from pathlib import Path 

6 

7from lmcat.lmcat import ( 

8 LMCatConfig, 

9 IgnoreHandler, 

10 walk_dir, 

11 walk_and_collect, 

12) 

13 

14# We will store all test directories under this path: 

15TEMP_PATH: Path = Path("tests/_temp") 

16 

17 

18def ensure_clean_dir(dirpath: Path) -> None: 

19 """Remove `dirpath` if it exists, then re-create it.""" 

20 if dirpath.is_dir(): 

21 shutil.rmtree(dirpath) 

22 dirpath.mkdir(parents=True, exist_ok=True) 

23 

24 

25# Test LMCatConfig - these tests remain largely unchanged 

26def test_lmcat_config_defaults(): 

27 config = LMCatConfig() 

28 assert config.tree_divider == "│ " 

29 assert config.indent == " " 

30 assert config.file_divider == "├── " 

31 assert config.content_divider == "``````" 

32 

33 

34def test_lmcat_config_load_partial(): 

35 data = {"tree_divider": "|---"} 

36 config = LMCatConfig.load(data) 

37 assert config.tree_divider == "|---" 

38 assert config.indent == " " 

39 assert config.file_divider == "├── " 

40 assert config.content_divider == "``````" 

41 

42 

43def test_lmcat_config_load_all(): 

44 data = { 

45 "tree_divider": "XX", 

46 "indent": "YY", 

47 "file_divider": "ZZ", 

48 "content_divider": "@@@", 

49 } 

50 config = LMCatConfig.load(data) 

51 assert config.tree_divider == "XX" 

52 assert config.indent == "YY" 

53 assert config.file_divider == "ZZ" 

54 assert config.content_divider == "@@@" 

55 

56 

57# Test IgnoreHandler class 

58def test_ignore_handler_init(): 

59 """Test basic initialization of IgnoreHandler""" 

60 test_dir = TEMP_PATH / "test_ignore_handler_init" 

61 ensure_clean_dir(test_dir) 

62 config = LMCatConfig() 

63 handler = IgnoreHandler(test_dir, config) 

64 assert handler.root_dir == test_dir 

65 assert handler.config == config 

66 

67 

68def test_ignore_handler_basic_ignore(): 

69 """Test basic ignore patterns""" 

70 test_dir = TEMP_PATH / "test_ignore_handler_basic_ignore" 

71 ensure_clean_dir(test_dir) 

72 

73 # Create test files 

74 (test_dir / "file1.txt").write_text("content1") 

75 (test_dir / "file2.log").write_text("content2") 

76 (test_dir / ".lmignore").write_text("*.log\n") 

77 

78 config = LMCatConfig() 

79 handler = IgnoreHandler(test_dir, config) 

80 

81 # Test matches 

82 assert not handler.is_ignored(test_dir / "file1.txt") 

83 assert handler.is_ignored(test_dir / "file2.log") 

84 

85 

86def test_ignore_handler_directory_patterns(): 

87 """Test directory ignore patterns""" 

88 test_dir = TEMP_PATH / "test_ignore_handler_directory" 

89 ensure_clean_dir(test_dir) 

90 

91 # Create test structure 

92 (test_dir / "subdir1").mkdir() 

93 (test_dir / "subdir2").mkdir() 

94 (test_dir / "subdir1/file1.txt").write_text("content1") 

95 (test_dir / "subdir2/file2.txt").write_text("content2") 

96 (test_dir / ".lmignore").write_text("subdir2/\n") 

97 

98 config = LMCatConfig() 

99 handler = IgnoreHandler(test_dir, config) 

100 

101 # Test matches 

102 assert not handler.is_ignored(test_dir / "subdir1") 

103 assert handler.is_ignored(test_dir / "subdir2") 

104 assert not handler.is_ignored(test_dir / "subdir1/file1.txt") 

105 assert handler.is_ignored(test_dir / "subdir2/file2.txt") 

106 

107 

108def test_ignore_handler_negation(): 

109 """Test negation patterns""" 

110 test_dir = TEMP_PATH / "test_ignore_handler_negation" 

111 ensure_clean_dir(test_dir) 

112 

113 # Create test files 

114 (test_dir / "file1.txt").write_text("content1") 

115 (test_dir / "file2.txt").write_text("content2") 

116 (test_dir / ".gitignore").write_text("*.txt\n") 

117 (test_dir / ".lmignore").write_text("!file2.txt\n") 

118 

119 config = LMCatConfig() 

120 handler = IgnoreHandler(test_dir, config) 

121 

122 # Test matches - file2.txt should be unignored by the negation 

123 assert handler.is_ignored(test_dir / "file1.txt") 

124 assert not handler.is_ignored(test_dir / "file2.txt") 

125 

126 

127def test_ignore_handler_nested_ignore_files(): 

128 """Test nested ignore files with different patterns""" 

129 test_dir = TEMP_PATH / "test_ignore_handler_nested" 

130 ensure_clean_dir(test_dir) 

131 

132 # Create test structure 

133 (test_dir / "subdir").mkdir() 

134 (test_dir / "subdir/file1.txt").write_text("content1") 

135 (test_dir / "subdir/file2.log").write_text("content2") 

136 

137 # Root ignores .txt, subdir ignores .log 

138 (test_dir / ".lmignore").write_text("*.txt\n") 

139 (test_dir / "subdir/.lmignore").write_text("*.log\n") 

140 

141 config = LMCatConfig() 

142 handler = IgnoreHandler(test_dir, config) 

143 

144 # Test both patterns are active 

145 assert handler.is_ignored(test_dir / "subdir/file1.txt") 

146 assert handler.is_ignored(test_dir / "subdir/file2.log") 

147 

148 

149def test_ignore_handler_gitignore_disabled(): 

150 """Test that gitignore patterns are ignored when disabled""" 

151 test_dir = TEMP_PATH / "test_ignore_handler_gitignore_disabled" 

152 ensure_clean_dir(test_dir) 

153 

154 # Create test files 

155 (test_dir / "file1.txt").write_text("content1") 

156 (test_dir / ".gitignore").write_text("*.txt\n") 

157 

158 config = LMCatConfig(include_gitignore=False) 

159 handler = IgnoreHandler(test_dir, config) 

160 

161 # File should not be ignored since gitignore is disabled 

162 assert not handler.is_ignored(test_dir / "file1.txt") 

163 

164 

165# Test walking functions with new IgnoreHandler 

166def test_walk_dir_basic(): 

167 """Test basic directory walking with no ignore patterns""" 

168 test_dir = TEMP_PATH / "test_walk_dir_basic" 

169 ensure_clean_dir(test_dir) 

170 

171 # Create test structure 

172 (test_dir / "subdir1").mkdir() 

173 (test_dir / "subdir2").mkdir() 

174 (test_dir / "subdir1/file1.txt").write_text("content1") 

175 (test_dir / "subdir2/file2.txt").write_text("content2") 

176 (test_dir / "file3.txt").write_text("content3") 

177 

178 config = LMCatConfig() 

179 handler = IgnoreHandler(test_dir, config) 

180 

181 tree_output, files = walk_dir(test_dir, handler, config) 

182 joined_output = "\n".join(tree_output) 

183 

184 # Check output contains all entries 

185 assert "subdir1" in joined_output 

186 assert "subdir2" in joined_output 

187 assert "file1.txt" in joined_output 

188 assert "file2.txt" in joined_output 

189 assert "file3.txt" in joined_output 

190 

191 # Check collected files 

192 assert len(files) == 3 

193 file_names = {f.name for f in files} 

194 assert file_names == {"file1.txt", "file2.txt", "file3.txt"} 

195 

196 

197def test_walk_dir_with_ignore(): 

198 """Test directory walking with ignore patterns""" 

199 test_dir = TEMP_PATH / "test_walk_dir_with_ignore" 

200 ensure_clean_dir(test_dir) 

201 

202 # Create test structure 

203 (test_dir / "subdir1").mkdir() 

204 (test_dir / "subdir2").mkdir() 

205 (test_dir / "subdir1/file1.txt").write_text("content1") 

206 (test_dir / "subdir2/file2.log").write_text("content2") 

207 (test_dir / "file3.txt").write_text("content3") 

208 

209 # Ignore .log files 

210 (test_dir / ".lmignore").write_text("*.log\n") 

211 

212 config = LMCatConfig() 

213 handler = IgnoreHandler(test_dir, config) 

214 

215 tree_output, files = walk_dir(test_dir, handler, config) 

216 joined_output = "\n".join(tree_output) 

217 

218 # Check output excludes .log file 

219 assert "file2.log" not in joined_output 

220 assert "file1.txt" in joined_output 

221 assert "file3.txt" in joined_output 

222 

223 # Check collected files 

224 assert len(files) == 2 

225 file_names = {f.name for f in files} 

226 assert file_names == {"file1.txt", "file3.txt"} 

227 

228 

229def test_walk_and_collect_complex(): 

230 """Test full directory walking with multiple ignore patterns""" 

231 test_dir = TEMP_PATH / "test_walk_and_collect_complex" 

232 ensure_clean_dir(test_dir) 

233 

234 # Create complex directory structure 

235 (test_dir / "subdir1/nested").mkdir(parents=True) 

236 (test_dir / "subdir2/nested").mkdir(parents=True) 

237 (test_dir / "subdir1/file1.txt").write_text("content1") 

238 (test_dir / "subdir1/nested/file2.log").write_text("content2") 

239 (test_dir / "subdir2/file3.txt").write_text("content3") 

240 (test_dir / "subdir2/nested/file4.log").write_text("content4") 

241 

242 # Root ignores .log files 

243 (test_dir / ".lmignore").write_text("*.log\n") 

244 # subdir2 ignores nested dir 

245 (test_dir / "subdir2/.lmignore").write_text("nested/\n") 

246 

247 config = LMCatConfig() 

248 tree_output, files = walk_and_collect(test_dir, config) 

249 joined_output = "\n".join(tree_output) 

250 

251 # Check correct files are excluded 

252 assert "file1.txt" in joined_output 

253 assert "file2.log" not in joined_output 

254 assert "file3.txt" in joined_output 

255 assert "file4.log" not in joined_output 

256 assert "nested" not in joined_output.split("\n")[-5:] # Check last few lines 

257 

258 # Check collected files 

259 assert len(files) == 2 

260 file_names = {f.name for f in files} 

261 assert file_names == {"file1.txt", "file3.txt"} 

262 

263 

264# Test CLI functionality 

265def test_cli_output_file(): 

266 """Test writing output to a file""" 

267 test_dir = TEMP_PATH / "test_cli_output_file" 

268 ensure_clean_dir(test_dir) 

269 

270 # Create test files 

271 (test_dir / "file1.txt").write_text("content1") 

272 output_file = test_dir / "output.md" 

273 

274 original_cwd = os.getcwd() 

275 try: 

276 os.chdir(test_dir) 

277 subprocess.run( 

278 ["uv", "run", "python", "-m", "lmcat", "--output", str(output_file)], 

279 check=True, 

280 ) 

281 

282 # Check output file exists and contains expected content 

283 assert output_file.is_file() 

284 content = output_file.read_text() 

285 assert "# File Tree" in content 

286 assert "file1.txt" in content 

287 assert "content1" in content 

288 except subprocess.CalledProcessError as e: 

289 print(f"{e = }", file=sys.stderr) 

290 print(e.stdout, file=sys.stderr) 

291 print(e.stderr, file=sys.stderr) 

292 raise e 

293 finally: 

294 os.chdir(original_cwd) 

295 

296 

297def test_cli_tree_only(): 

298 """Test --tree-only option""" 

299 test_dir = TEMP_PATH / "test_cli_tree_only" 

300 ensure_clean_dir(test_dir) 

301 

302 # Create test file 

303 (test_dir / "file1.txt").write_text("content1") 

304 

305 original_cwd = os.getcwd() 

306 try: 

307 os.chdir(test_dir) 

308 result = subprocess.run( 

309 ["uv", "run", "python", "-m", "lmcat", "--tree-only"], 

310 capture_output=True, 

311 text=True, 

312 check=True, 

313 ) 

314 

315 # Check output has tree but not content 

316 assert "# File Tree" in result.stdout 

317 assert "file1.txt" in result.stdout 

318 assert "# File Contents" not in result.stdout 

319 assert "content1" not in result.stdout 

320 except subprocess.CalledProcessError as e: 

321 print(f"{e = }", file=sys.stderr) 

322 print(e.stdout, file=sys.stderr) 

323 print(e.stderr, file=sys.stderr) 

324 raise e 

325 finally: 

326 os.chdir(original_cwd)