Coverage for tests\test_lmcat.py: 94%

1 import sys

2 import os

3 import shutil

4 import subprocess

5 from pathlib import Path

6

7 from lmcat.file_stats import TokenizerWrapper

8 from 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:

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

17

18

19 def 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

27 def 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

35 def 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

44 def 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

59 def 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

69 def 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

87 def 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

109 def 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

128 def 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

150 def 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

167 def 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

198 def 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

230 def 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

266 def 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

298 def 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)