Coverage for tests\test_lmcat.py: 94%

193 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-01-29 16:42 -0700

1import sys 

2import os 

3import shutil 

4import subprocess 

5from pathlib import Path 

6 

7from lmcat.file_stats import TokenizerWrapper 

8from lmcat.lmcat import ( 

9 LMCatConfig, 

10 IgnoreHandler, 

11 walk_dir, 

12 walk_and_collect, 

13) 

14 

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

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

17 

18 

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

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

21 if dirpath.is_dir(): 

22 shutil.rmtree(dirpath) 

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

24 

25 

26# Test LMCatConfig - these tests remain largely unchanged 

27def test_lmcat_config_defaults(): 

28 config = LMCatConfig() 

29 assert config.tree_divider == "│ " 

30 assert config.tree_indent == " " 

31 assert config.tree_file_divider == "├── " 

32 assert config.content_divider == "``````" 

33 

34 

35def test_lmcat_config_load_partial(): 

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

37 config = LMCatConfig.load(data) 

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

39 assert config.tree_indent == " " 

40 assert config.tree_file_divider == "├── " 

41 assert config.content_divider == "``````" 

42 

43 

44def test_lmcat_config_load_all(): 

45 data = { 

46 "tree_divider": "XX", 

47 "tree_indent": "YY", 

48 "tree_file_divider": "ZZ", 

49 "content_divider": "@@@", 

50 } 

51 config = LMCatConfig.load(data) 

52 assert config.tree_divider == "XX" 

53 assert config.tree_indent == "YY" 

54 assert config.tree_file_divider == "ZZ" 

55 assert config.content_divider == "@@@" 

56 

57 

58# Test IgnoreHandler class 

59def test_ignore_handler_init(): 

60 """Test basic initialization of IgnoreHandler""" 

61 test_dir = TEMP_PATH / "test_ignore_handler_init" 

62 ensure_clean_dir(test_dir) 

63 config = LMCatConfig() 

64 handler = IgnoreHandler(test_dir, config) 

65 assert handler.root_dir == test_dir 

66 assert handler.config == config 

67 

68 

69def test_ignore_handler_basic_ignore(): 

70 """Test basic ignore patterns""" 

71 test_dir = TEMP_PATH / "test_ignore_handler_basic_ignore" 

72 ensure_clean_dir(test_dir) 

73 

74 # Create test files 

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

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

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

78 

79 config = LMCatConfig() 

80 handler = IgnoreHandler(test_dir, config) 

81 

82 # Test matches 

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

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

85 

86 

87def test_ignore_handler_directory_patterns(): 

88 """Test directory ignore patterns""" 

89 test_dir = TEMP_PATH / "test_ignore_handler_directory" 

90 ensure_clean_dir(test_dir) 

91 

92 # Create test structure 

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

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

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

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

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

98 

99 config = LMCatConfig() 

100 handler = IgnoreHandler(test_dir, config) 

101 

102 # Test matches 

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

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

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

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

107 

108 

109def test_ignore_handler_negation(): 

110 """Test negation patterns""" 

111 test_dir = TEMP_PATH / "test_ignore_handler_negation" 

112 ensure_clean_dir(test_dir) 

113 

114 # Create test files 

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

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

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

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

119 

120 config = LMCatConfig() 

121 handler = IgnoreHandler(test_dir, config) 

122 

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

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

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

126 

127 

128def test_ignore_handler_nested_ignore_files(): 

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

130 test_dir = TEMP_PATH / "test_ignore_handler_nested" 

131 ensure_clean_dir(test_dir) 

132 

133 # Create test structure 

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

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

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

137 

138 # Root ignores .txt, subdir ignores .log 

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

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

141 

142 config = LMCatConfig() 

143 handler = IgnoreHandler(test_dir, config) 

144 

145 # Test both patterns are active 

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

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

148 

149 

150def test_ignore_handler_gitignore_disabled(): 

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

152 test_dir = TEMP_PATH / "test_ignore_handler_gitignore_disabled" 

153 ensure_clean_dir(test_dir) 

154 

155 # Create test files 

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

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

158 

159 config = LMCatConfig(ignore_patterns_files=list()) 

160 handler = IgnoreHandler(test_dir, config) 

161 

162 # File should not be ignored since gitignore is disabled 

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

164 

165 

166# Test walking functions with new IgnoreHandler 

167def test_walk_dir_basic(): 

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

169 test_dir = TEMP_PATH / "test_walk_dir_basic" 

170 ensure_clean_dir(test_dir) 

171 

172 # Create test structure 

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

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

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

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

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

178 

179 config = LMCatConfig() 

180 handler = IgnoreHandler(test_dir, config) 

181 

182 tree_output, files = walk_dir(test_dir, handler, config, TokenizerWrapper()) 

183 joined_output = "\n".join([x.line for x in tree_output]) 

184 

185 # Check output contains all entries 

186 assert "subdir1" in joined_output 

187 assert "subdir2" in joined_output 

188 assert "file1.txt" in joined_output 

189 assert "file2.txt" in joined_output 

190 assert "file3.txt" in joined_output 

191 

192 # Check collected files 

193 assert len(files) == 3 

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

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

196 

197 

198def test_walk_dir_with_ignore(): 

199 """Test directory walking with ignore patterns""" 

200 test_dir = TEMP_PATH / "test_walk_dir_with_ignore" 

201 ensure_clean_dir(test_dir) 

202 

203 # Create test structure 

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

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

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

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

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

209 

210 # Ignore .log files 

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

212 

213 config = LMCatConfig() 

214 handler = IgnoreHandler(test_dir, config) 

215 

216 tree_output, files = walk_dir(test_dir, handler, config, TokenizerWrapper()) 

217 joined_output = "\n".join([x.line for x in tree_output]) 

218 

219 # Check output excludes .log file 

220 assert "file2.log" not in joined_output 

221 assert "file1.txt" in joined_output 

222 assert "file3.txt" in joined_output 

223 

224 # Check collected files 

225 assert len(files) == 2 

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

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

228 

229 

230def test_walk_and_collect_complex(): 

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

232 test_dir = TEMP_PATH / "test_walk_and_collect_complex" 

233 ensure_clean_dir(test_dir) 

234 

235 # Create complex directory structure 

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

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

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

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

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

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

242 

243 # Root ignores .log files 

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

245 # subdir2 ignores nested dir 

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

247 

248 config = LMCatConfig() 

249 tree_output, files = walk_and_collect(test_dir, config) 

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

251 

252 # Check correct files are excluded 

253 assert "file1.txt" in joined_output 

254 assert "file2.log" not in joined_output 

255 assert "file3.txt" in joined_output 

256 assert "file4.log" not in joined_output 

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

258 

259 # Check collected files 

260 assert len(files) == 2 

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

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

263 

264 

265# Test CLI functionality 

266def test_cli_output_file(): 

267 """Test writing output to a file""" 

268 test_dir = TEMP_PATH / "test_cli_output_file" 

269 ensure_clean_dir(test_dir) 

270 

271 # Create test files 

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

273 output_file = test_dir / "output.md" 

274 

275 original_cwd = os.getcwd() 

276 try: 

277 os.chdir(test_dir) 

278 subprocess.run( 

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

280 check=True, 

281 ) 

282 

283 # Check output file exists and contains expected content 

284 assert output_file.is_file() 

285 content = output_file.read_text() 

286 assert "# File Tree" in content 

287 assert "file1.txt" in content 

288 assert "content1" in content 

289 except subprocess.CalledProcessError as e: 

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

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

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

293 raise e 

294 finally: 

295 os.chdir(original_cwd) 

296 

297 

298def test_cli_tree_only(): 

299 """Test --tree-only option""" 

300 test_dir = TEMP_PATH / "test_cli_tree_only" 

301 ensure_clean_dir(test_dir) 

302 

303 # Create test file 

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

305 

306 original_cwd = os.getcwd() 

307 try: 

308 os.chdir(test_dir) 

309 result = subprocess.run( 

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

311 capture_output=True, 

312 text=True, 

313 check=True, 

314 ) 

315 

316 # Check output has tree but not content 

317 assert "# File Tree" in result.stdout 

318 assert "file1.txt" in result.stdout 

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

320 assert "content1" not in result.stdout 

321 except subprocess.CalledProcessError as e: 

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

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

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

325 raise e 

326 finally: 

327 os.chdir(original_cwd)