Coverage for tests/core/test_logger.py: 99%

250 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-07-03 15:51 +0000

1import pytest 

2import os 

3import sys 

4import tempfile 

5import shutil 

6import gzip 

7import logging 

8from pathlib import Path 

9from unittest.mock import Mock, patch, MagicMock 

10 

11from main.logger import CompressedRotatingFileHandler, setup_logger, get_logger, set_log_level 

12 

13 

14class TestCompressedRotatingFileHandler: 

15 """Tests for CompressedRotatingFileHandler""" 

16 

17 @pytest.fixture 

18 def temp_log_dir(self): 

19 """Create a temporary directory for logs""" 

20 temp_dir = tempfile.mkdtemp() 

21 yield Path(temp_dir) 

22 shutil.rmtree(temp_dir, ignore_errors=True) 

23 

24 @pytest.fixture 

25 def log_file(self, temp_log_dir): 

26 """Create path for log file""" 

27 return temp_log_dir / "test.log" 

28 

29 def test_handler_initialization(self, log_file): 

30 """Verify handler initialization""" 

31 handler = CompressedRotatingFileHandler( 

32 str(log_file), 

33 maxBytes=1024, 

34 backupCount=3 

35 ) 

36 

37 assert handler.maxBytes == 1024 

38 assert handler.backupCount == 3 

39 assert handler.log_dir == log_file.parent 

40 

41 def test_log_dir_created(self, temp_log_dir): 

42 """Verify log directory creation""" 

43 log_file = temp_log_dir / "subdir" / "test.log" 

44 

45 handler = CompressedRotatingFileHandler( 

46 str(log_file), 

47 maxBytes=1024, 

48 backupCount=3 

49 ) 

50 

51 assert log_file.parent.exists() 

52 

53 def test_should_rollover_small_file(self, log_file): 

54 """Verify it does not rollover with a small file""" 

55 handler = CompressedRotatingFileHandler( 

56 str(log_file), 

57 maxBytes=10000, 

58 backupCount=3 

59 ) 

60 

61 record = logging.LogRecord( 

62 name="test", 

63 level=logging.INFO, 

64 pathname="test.py", 

65 lineno=1, 

66 msg="Short message", 

67 args=(), 

68 exc_info=None 

69 ) 

70 

71 # With a small file, it should not rollover 

72 should_roll = handler.shouldRollover(record) 

73 assert should_roll == False 

74 

75 handler.close() 

76 

77 def test_should_rollover_large_file(self, log_file): 

78 """Verify rollover with a large file""" 

79 handler = CompressedRotatingFileHandler( 

80 str(log_file), 

81 maxBytes=100, # Very small 

82 backupCount=3 

83 ) 

84 

85 # Write data to fill 

86 for i in range(10): 

87 handler.emit(logging.LogRecord( 

88 name="test", 

89 level=logging.INFO, 

90 pathname="test.py", 

91 lineno=1, 

92 msg=f"Message {i}" * 10, 

93 args=(), 

94 exc_info=None 

95 )) 

96 

97 record = logging.LogRecord( 

98 name="test", 

99 level=logging.INFO, 

100 pathname="test.py", 

101 lineno=1, 

102 msg="Final message", 

103 args=(), 

104 exc_info=None 

105 ) 

106 

107 should_roll = handler.shouldRollover(record) 

108 handler.close() 

109 

110 # Could be True if the file is large enough 

111 assert isinstance(should_roll, bool) 

112 

113 def test_do_rollover_creates_compressed_file(self, log_file): 

114 """Verify that rollover creates a compressed file""" 

115 handler = CompressedRotatingFileHandler( 

116 str(log_file), 

117 maxBytes=100, 

118 backupCount=3 

119 ) 

120 

121 # Write something 

122 handler.emit(logging.LogRecord( 

123 name="test", 

124 level=logging.INFO, 

125 pathname="test.py", 

126 lineno=1, 

127 msg="Test message", 

128 args=(), 

129 exc_info=None 

130 )) 

131 

132 # Force rollover 

133 handler.doRollover() 

134 

135 # Verify that a .gz file exists 

136 gz_files = list(log_file.parent.glob("*.gz")) 

137 assert len(gz_files) > 0 

138 

139 handler.close() 

140 

141 def test_do_rollover_maintains_backup_count(self, log_file): 

142 """Verify that it keeps only N backups""" 

143 handler = CompressedRotatingFileHandler( 

144 str(log_file), 

145 maxBytes=50, 

146 backupCount=2 

147 ) 

148 

149 # Create more rollovers than the limit 

150 for i in range(5): 

151 handler.emit(logging.LogRecord( 

152 name="test", 

153 level=logging.INFO, 

154 pathname="test.py", 

155 lineno=1, 

156 msg=f"Message {i}" * 20, 

157 args=(), 

158 exc_info=None 

159 )) 

160 handler.doRollover() 

161 

162 # Verify there are only 2 backups (backupCount) 

163 gz_files = list(log_file.parent.glob("*.gz")) 

164 assert len(gz_files) <= 2 

165 

166 handler.close() 

167 

168 def test_compressed_file_readable(self, log_file): 

169 """Verify that the compressed file is readable""" 

170 handler = CompressedRotatingFileHandler( 

171 str(log_file), 

172 maxBytes=100, 

173 backupCount=3 

174 ) 

175 

176 test_message = "Test message for compression" 

177 handler.emit(logging.LogRecord( 

178 name="test", 

179 level=logging.INFO, 

180 pathname="test.py", 

181 lineno=1, 

182 msg=test_message, 

183 args=(), 

184 exc_info=None 

185 )) 

186 

187 handler.doRollover() 

188 

189 # Find the compressed file 

190 gz_files = list(log_file.parent.glob("*.gz")) 

191 if gz_files: 

192 with gzip.open(gz_files[0], "rt") as f: 

193 content = f.read() 

194 assert test_message in content 

195 

196 handler.close() 

197 

198 

199class TestSetupLogger: 

200 """Tests for setup_logger function""" 

201 

202 @pytest.fixture 

203 def temp_log_dir(self): 

204 temp_dir = tempfile.mkdtemp() 

205 yield Path(temp_dir) 

206 shutil.rmtree(temp_dir, ignore_errors=True) 

207 

208 @pytest.fixture 

209 def clean_logger(self): 

210 """Cleans logger before and after test""" 

211 logger_name = "TestLogger" 

212 test_logger = logging.getLogger(logger_name) 

213 test_logger.handlers.clear() 

214 yield logger_name 

215 test_logger.handlers.clear() 

216 

217 def test_setup_logger_creates_logger(self, temp_log_dir, clean_logger): 

218 """Verify logger creation""" 

219 with patch('main.logger.get_app_dir', return_value=temp_log_dir): 

220 log = setup_logger( 

221 console=False, 

222 logger_name=clean_logger, 

223 level=logging.INFO 

224 ) 

225 

226 assert log is not None 

227 assert log.name == clean_logger 

228 assert log.level == logging.INFO 

229 

230 def test_setup_logger_creates_log_file(self, temp_log_dir, clean_logger): 

231 """Verify log file creation""" 

232 with patch('main.logger.get_app_dir', return_value=temp_log_dir): 

233 log = setup_logger( 

234 console=False, 

235 logger_name=clean_logger 

236 ) 

237 

238 log_file = temp_log_dir / ".log" / "log.txt" 

239 # The file might not exist immediately, but the directory will 

240 assert log_file.parent.exists() 

241 

242 def test_setup_logger_with_console(self, temp_log_dir, clean_logger): 

243 """Verify addition of console handler""" 

244 with patch('main.logger.get_app_dir', return_value=temp_log_dir): 

245 log = setup_logger( 

246 console=True, 

247 logger_name=clean_logger 

248 ) 

249 

250 # Should have at least 2 handlers (file + console) 

251 assert len(log.handlers) >= 2 

252 

253 # Verify that one is a StreamHandler 

254 stream_handlers = [h for h in log.handlers 

255 if isinstance(h, logging.StreamHandler) 

256 and not isinstance(h, CompressedRotatingFileHandler)] 

257 assert len(stream_handlers) > 0 

258 

259 def test_setup_logger_without_console(self, temp_log_dir, clean_logger): 

260 """Verify setup without console handler""" 

261 with patch('main.logger.get_app_dir', return_value=temp_log_dir): 

262 log = setup_logger( 

263 console=False, 

264 logger_name=clean_logger 

265 ) 

266 

267 # Verify no StreamHandlers (except file handler) 

268 stream_handlers = [h for h in log.handlers 

269 if isinstance(h, logging.StreamHandler) 

270 and not isinstance(h, CompressedRotatingFileHandler)] 

271 assert len(stream_handlers) == 0 

272 

273 def test_setup_logger_custom_logfile(self, temp_log_dir, clean_logger): 

274 """Verify use of custom log file""" 

275 custom_file = temp_log_dir / "custom.log" 

276 

277 with patch('main.logger.get_app_dir', return_value=temp_log_dir): 

278 log = setup_logger( 

279 console=False, 

280 logger_name=clean_logger, 

281 logfile=custom_file 

282 ) 

283 

284 # Verify it uses the custom file 

285 file_handlers = [h for h in log.handlers 

286 if isinstance(h, CompressedRotatingFileHandler)] 

287 assert len(file_handlers) > 0 

288 

289 def test_setup_logger_clears_existing_handlers(self, temp_log_dir, clean_logger): 

290 """Verify that it clears existing handlers""" 

291 # Add an existing handler 

292 test_logger = logging.getLogger(clean_logger) 

293 test_logger.addHandler(logging.NullHandler()) 

294 initial_count = len(test_logger.handlers) 

295 

296 with patch('main.logger.get_app_dir', return_value=temp_log_dir): 

297 log = setup_logger( 

298 console=False, 

299 logger_name=clean_logger 

300 ) 

301 

302 # Old handlers should be removed 

303 assert all(not isinstance(h, logging.NullHandler) for h in log.handlers) 

304 

305 def test_setup_logger_max_bytes(self, temp_log_dir, clean_logger): 

306 """Verify maxBytes setting""" 

307 with patch('main.logger.get_app_dir', return_value=temp_log_dir): 

308 log = setup_logger( 

309 console=False, 

310 logger_name=clean_logger, 

311 maxBytes=5000 

312 ) 

313 

314 file_handlers = [h for h in log.handlers 

315 if isinstance(h, CompressedRotatingFileHandler)] 

316 if file_handlers: 

317 assert file_handlers[0].maxBytes == 5000 

318 

319 def test_setup_logger_backup_count(self, temp_log_dir, clean_logger): 

320 """Verify backupCount setting""" 

321 with patch('main.logger.get_app_dir', return_value=temp_log_dir): 

322 log = setup_logger( 

323 console=False, 

324 logger_name=clean_logger, 

325 backupCount=10 

326 ) 

327 

328 file_handlers = [h for h in log.handlers 

329 if isinstance(h, CompressedRotatingFileHandler)] 

330 if file_handlers: 

331 assert file_handlers[0].backupCount == 10 

332 

333 

334class TestGetLogger: 

335 """Tests for get_logger function""" 

336 

337 def test_get_logger_returns_logger(self): 

338 """Verify it returns a logger""" 

339 log = get_logger("TestGetLogger") 

340 assert isinstance(log, logging.Logger) 

341 

342 def test_get_logger_default_name(self): 

343 """Verify default name""" 

344 log = get_logger() 

345 assert log.name == "GliAAns-UI" 

346 

347 def test_get_logger_custom_name(self): 

348 """Verify custom name""" 

349 log = get_logger("CustomLogger") 

350 assert log.name == "CustomLogger" 

351 

352 def test_get_logger_same_instance(self): 

353 """Verify it returns the same instance""" 

354 log1 = get_logger("SameLogger") 

355 log2 = get_logger("SameLogger") 

356 assert log1 is log2 

357 

358 

359class TestSetLogLevel: 

360 """Tests for set_log_level function""" 

361 

362 @pytest.fixture 

363 def test_logger(self): 

364 """Create test logger""" 

365 logger_name = "TestSetLevel" 

366 test_log = logging.getLogger(logger_name) 

367 test_log.setLevel(logging.WARNING) 

368 yield logger_name, test_log 

369 test_log.handlers.clear() 

370 

371 def test_set_log_level_debug(self, test_logger): 

372 """Verify DEBUG level setting""" 

373 logger_name, log = test_logger 

374 

375 set_log_level(logging.DEBUG, logger_name) 

376 

377 assert log.level == logging.DEBUG 

378 

379 def test_set_log_level_info(self, test_logger): 

380 """Verify INFO level setting""" 

381 logger_name, log = test_logger 

382 

383 set_log_level(logging.INFO, logger_name) 

384 

385 assert log.level == logging.INFO 

386 

387 def test_set_log_level_error(self, test_logger): 

388 """Verify ERROR level setting""" 

389 logger_name, log = test_logger 

390 

391 set_log_level(logging.ERROR, logger_name) 

392 

393 assert log.level == logging.ERROR 

394 

395 def test_set_log_level_default_logger(self): 

396 """Verify setting on default logger""" 

397 set_log_level(logging.WARNING) 

398 

399 log = get_logger() 

400 assert log.level == logging.WARNING 

401 

402 

403class TestLoggerFormatting: 

404 """Tests for log formatting""" 

405 

406 @pytest.fixture 

407 def temp_log_dir(self): 

408 temp_dir = tempfile.mkdtemp() 

409 yield Path(temp_dir) 

410 shutil.rmtree(temp_dir, ignore_errors=True) 

411 

412 def test_file_formatter(self, temp_log_dir): 

413 """Verify log file format""" 

414 with patch('main.logger.get_app_dir', return_value=temp_log_dir): 

415 log = setup_logger( 

416 console=False, 

417 logger_name="TestFormat", 

418 level=logging.DEBUG 

419 ) 

420 

421 log.info("Test message") 

422 

423 log_file = temp_log_dir / ".log" / "log.txt" 

424 if log_file.exists(): 

425 content = log_file.read_text() 

426 # Should contain timestamp, level, filename, etc 

427 assert "INFO" in content 

428 assert "Test message" in content 

429 

430 def test_console_formatter(self, temp_log_dir, capsys): 

431 """Verify console log format""" 

432 with patch('main.logger.get_app_dir', return_value=temp_log_dir): 

433 log = setup_logger( 

434 console=True, 

435 logger_name="TestConsoleFormat", 

436 level=logging.DEBUG 

437 ) 

438 

439 log.info("Console test message") 

440 

441 captured = capsys.readouterr() 

442 # Console should have a more compact format 

443 assert "INFO" in captured.out or "Console test message" in captured.out 

444 

445 

446class TestLoggerIntegration: 

447 """Integration tests for realistic scenarios""" 

448 

449 @pytest.fixture 

450 def temp_log_dir(self): 

451 temp_dir = tempfile.mkdtemp() 

452 yield Path(temp_dir) 

453 shutil.rmtree(temp_dir, ignore_errors=True) 

454 

455 def test_full_logging_workflow(self, temp_log_dir): 

456 """Test full logging workflow""" 

457 with patch('main.logger.get_app_dir', return_value=temp_log_dir): 

458 # Setup logger 

459 log = setup_logger( 

460 console=False, 

461 logger_name="IntegrationTest", 

462 level=logging.INFO, 

463 maxBytes=500, 

464 backupCount=2 

465 ) 

466 

467 # Write various logs 

468 for i in range(50): 

469 log.info(f"Log message {i}") 

470 log.debug(f"Debug message {i}") 

471 log.error(f"Error message {i}") 

472 

473 # Verify files exist 

474 log_dir = temp_log_dir / ".log" 

475 assert log_dir.exists() 

476 

477 # There might be compressed files 

478 all_files = list(log_dir.glob("*")) 

479 assert len(all_files) > 0 

480 

481 def test_logger_persistence(self, temp_log_dir): 

482 """Verify persistence between calls""" 

483 with patch('main.logger.get_app_dir', return_value=temp_log_dir): 

484 # First call 

485 log1 = setup_logger( 

486 console=False, 

487 logger_name="PersistenceTest", 

488 level=logging.INFO 

489 ) 

490 log1.info("First message") 

491 

492 # Second call (should clear handlers) 

493 log2 = setup_logger( 

494 console=False, 

495 logger_name="PersistenceTest", 

496 level=logging.DEBUG 

497 ) 

498 

499 assert log1.name == log2.name 

500 assert log2.level == logging.DEBUG 

501 

502 

503class TestEdgeCases: 

504 """Tests for edge cases""" 

505 

506 @pytest.fixture 

507 def temp_log_dir(self): 

508 temp_dir = tempfile.mkdtemp() 

509 yield Path(temp_dir) 

510 shutil.rmtree(temp_dir, ignore_errors=True) 

511 

512 def test_empty_log_message(self, temp_log_dir): 

513 """Verify handling of empty message""" 

514 with patch('main.logger.get_app_dir', return_value=temp_log_dir): 

515 log = setup_logger( 

516 console=False, 

517 logger_name="EmptyTest", 

518 level=logging.INFO 

519 ) 

520 

521 # Should not cause errors 

522 log.info("") 

523 log.info(None) 

524 

525 def test_very_long_message(self, temp_log_dir): 

526 """Verify handling of very long message""" 

527 with patch('main.logger.get_app_dir', return_value=temp_log_dir): 

528 log = setup_logger( 

529 console=False, 

530 logger_name="LongTest", 

531 level=logging.INFO, 

532 maxBytes=1000 

533 ) 

534 

535 long_message = "A" * 10000 

536 log.info(long_message) 

537 

538 # Should trigger rollover 

539 # Verify it does not cause errors 

540 

541 def test_special_characters_in_message(self, temp_log_dir): 

542 """Verify handling of special characters""" 

543 with patch('main.logger.get_app_dir', return_value=temp_log_dir): 

544 log = setup_logger( 

545 console=False, 

546 logger_name="SpecialTest", 

547 level=logging.INFO 

548 ) 

549 

550 # Special characters 

551 log.info("Messaggio con àccénti €€€") 

552 log.info("Newlines\n\nand\ttabs") 

553 

554 def test_zero_backup_count(self, temp_log_dir): 

555 """Verify behavior with backupCount=0""" 

556 with patch('main.logger.get_app_dir', return_value=temp_log_dir): 

557 log = setup_logger( 

558 console=False, 

559 logger_name="NoBackupTest", 

560 level=logging.INFO, 

561 backupCount=0 

562 ) 

563 

564 # Should not cause errors 

565 for i in range(10): 

566 log.info(f"Message {i}") 

567 

568 def test_permission_error_handling(self, temp_log_dir): 

569 """Verify handling of permission errors""" 

570 with patch('main.logger.get_app_dir', return_value=temp_log_dir): 

571 # Create read-only directory (if possible) 

572 restricted_dir = temp_log_dir / "restricted" 

573 restricted_dir.mkdir() 

574 

575 try: 

576 restricted_dir.chmod(0o444) 

577 

578 # Attempt to create log in protected directory 

579 # Might fail, but should not crash 

580 try: 

581 setup_logger( 

582 console=False, 

583 logger_name="PermTest", 

584 logfile=restricted_dir / "test.log" 

585 ) 

586 except (PermissionError, OSError): 

587 pass # Expected error 

588 

589 finally: 

590 restricted_dir.chmod(0o755) 

591 

592 

593if __name__ == "__main__": 

594 pytest.main([__file__, "-v", "--tb=short"])