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
« 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
7from lmcat.file_stats import TokenizerWrapper
8from lmcat.lmcat import (
9 LMCatConfig,
10 IgnoreHandler,
11 walk_dir,
12 walk_and_collect,
13)
15# We will store all test directories under this path:
16TEMP_PATH: Path = Path("tests/_temp")
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)
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 == "``````"
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 == "``````"
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 == "@@@"
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
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)
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")
79 config = LMCatConfig()
80 handler = IgnoreHandler(test_dir, config)
82 # Test matches
83 assert not handler.is_ignored(test_dir / "file1.txt")
84 assert handler.is_ignored(test_dir / "file2.log")
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)
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")
99 config = LMCatConfig()
100 handler = IgnoreHandler(test_dir, config)
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")
109def test_ignore_handler_negation():
110 """Test negation patterns"""
111 test_dir = TEMP_PATH / "test_ignore_handler_negation"
112 ensure_clean_dir(test_dir)
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")
120 config = LMCatConfig()
121 handler = IgnoreHandler(test_dir, config)
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")
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)
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")
138 # Root ignores .txt, subdir ignores .log
139 (test_dir / ".lmignore").write_text("*.txt\n")
140 (test_dir / "subdir/.lmignore").write_text("*.log\n")
142 config = LMCatConfig()
143 handler = IgnoreHandler(test_dir, config)
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")
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)
155 # Create test files
156 (test_dir / "file1.txt").write_text("content1")
157 (test_dir / ".gitignore").write_text("*.txt\n")
159 config = LMCatConfig(ignore_patterns_files=list())
160 handler = IgnoreHandler(test_dir, config)
162 # File should not be ignored since gitignore is disabled
163 assert not handler.is_ignored(test_dir / "file1.txt")
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)
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")
179 config = LMCatConfig()
180 handler = IgnoreHandler(test_dir, config)
182 tree_output, files = walk_dir(test_dir, handler, config, TokenizerWrapper())
183 joined_output = "\n".join([x.line for x in tree_output])
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
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"}
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)
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")
210 # Ignore .log files
211 (test_dir / ".lmignore").write_text("*.log\n")
213 config = LMCatConfig()
214 handler = IgnoreHandler(test_dir, config)
216 tree_output, files = walk_dir(test_dir, handler, config, TokenizerWrapper())
217 joined_output = "\n".join([x.line for x in tree_output])
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
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"}
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)
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")
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")
248 config = LMCatConfig()
249 tree_output, files = walk_and_collect(test_dir, config)
250 joined_output = "\n".join(tree_output)
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
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"}
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)
271 # Create test files
272 (test_dir / "file1.txt").write_text("content1")
273 output_file = test_dir / "output.md"
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 )
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)
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)
303 # Create test file
304 (test_dir / "file1.txt").write_text("content1")
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 )
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)