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
« 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
11from main.logger import CompressedRotatingFileHandler, setup_logger, get_logger, set_log_level
14class TestCompressedRotatingFileHandler:
15 """Tests for CompressedRotatingFileHandler"""
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)
24 @pytest.fixture
25 def log_file(self, temp_log_dir):
26 """Create path for log file"""
27 return temp_log_dir / "test.log"
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 )
37 assert handler.maxBytes == 1024
38 assert handler.backupCount == 3
39 assert handler.log_dir == log_file.parent
41 def test_log_dir_created(self, temp_log_dir):
42 """Verify log directory creation"""
43 log_file = temp_log_dir / "subdir" / "test.log"
45 handler = CompressedRotatingFileHandler(
46 str(log_file),
47 maxBytes=1024,
48 backupCount=3
49 )
51 assert log_file.parent.exists()
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 )
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 )
71 # With a small file, it should not rollover
72 should_roll = handler.shouldRollover(record)
73 assert should_roll == False
75 handler.close()
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 )
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 ))
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 )
107 should_roll = handler.shouldRollover(record)
108 handler.close()
110 # Could be True if the file is large enough
111 assert isinstance(should_roll, bool)
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 )
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 ))
132 # Force rollover
133 handler.doRollover()
135 # Verify that a .gz file exists
136 gz_files = list(log_file.parent.glob("*.gz"))
137 assert len(gz_files) > 0
139 handler.close()
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 )
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()
162 # Verify there are only 2 backups (backupCount)
163 gz_files = list(log_file.parent.glob("*.gz"))
164 assert len(gz_files) <= 2
166 handler.close()
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 )
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 ))
187 handler.doRollover()
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
196 handler.close()
199class TestSetupLogger:
200 """Tests for setup_logger function"""
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)
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()
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 )
226 assert log is not None
227 assert log.name == clean_logger
228 assert log.level == logging.INFO
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 )
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()
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 )
250 # Should have at least 2 handlers (file + console)
251 assert len(log.handlers) >= 2
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
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 )
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
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"
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 )
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
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)
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 )
302 # Old handlers should be removed
303 assert all(not isinstance(h, logging.NullHandler) for h in log.handlers)
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 )
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
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 )
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
334class TestGetLogger:
335 """Tests for get_logger function"""
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)
342 def test_get_logger_default_name(self):
343 """Verify default name"""
344 log = get_logger()
345 assert log.name == "GliAAns-UI"
347 def test_get_logger_custom_name(self):
348 """Verify custom name"""
349 log = get_logger("CustomLogger")
350 assert log.name == "CustomLogger"
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
359class TestSetLogLevel:
360 """Tests for set_log_level function"""
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()
371 def test_set_log_level_debug(self, test_logger):
372 """Verify DEBUG level setting"""
373 logger_name, log = test_logger
375 set_log_level(logging.DEBUG, logger_name)
377 assert log.level == logging.DEBUG
379 def test_set_log_level_info(self, test_logger):
380 """Verify INFO level setting"""
381 logger_name, log = test_logger
383 set_log_level(logging.INFO, logger_name)
385 assert log.level == logging.INFO
387 def test_set_log_level_error(self, test_logger):
388 """Verify ERROR level setting"""
389 logger_name, log = test_logger
391 set_log_level(logging.ERROR, logger_name)
393 assert log.level == logging.ERROR
395 def test_set_log_level_default_logger(self):
396 """Verify setting on default logger"""
397 set_log_level(logging.WARNING)
399 log = get_logger()
400 assert log.level == logging.WARNING
403class TestLoggerFormatting:
404 """Tests for log formatting"""
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)
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 )
421 log.info("Test message")
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
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 )
439 log.info("Console test message")
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
446class TestLoggerIntegration:
447 """Integration tests for realistic scenarios"""
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)
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 )
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}")
473 # Verify files exist
474 log_dir = temp_log_dir / ".log"
475 assert log_dir.exists()
477 # There might be compressed files
478 all_files = list(log_dir.glob("*"))
479 assert len(all_files) > 0
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")
492 # Second call (should clear handlers)
493 log2 = setup_logger(
494 console=False,
495 logger_name="PersistenceTest",
496 level=logging.DEBUG
497 )
499 assert log1.name == log2.name
500 assert log2.level == logging.DEBUG
503class TestEdgeCases:
504 """Tests for edge cases"""
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)
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 )
521 # Should not cause errors
522 log.info("")
523 log.info(None)
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 )
535 long_message = "A" * 10000
536 log.info(long_message)
538 # Should trigger rollover
539 # Verify it does not cause errors
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 )
550 # Special characters
551 log.info("Messaggio con àccénti €€€")
552 log.info("Newlines\n\nand\ttabs")
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 )
564 # Should not cause errors
565 for i in range(10):
566 log.info(f"Message {i}")
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()
575 try:
576 restricted_dir.chmod(0o444)
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
589 finally:
590 restricted_dir.chmod(0o755)
593if __name__ == "__main__":
594 pytest.main([__file__, "-v", "--tb=short"])