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.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:

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

16

17

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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