diff --git a/.gitignore b/.gitignore index 1235653..3275c74 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ Thumbs.db # Project specific json/ html/ +logs/ +*.log diff --git a/README.md b/README.md index 7f87f57..e46de42 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ checkpatch/ ├── core.py # Implementaciones de fixes (40+) ├── compile.py # Módulo de compilación de archivos ├── report.py # Generadores de HTML (8 reportes) +├── logger.py # Sistema de logging unificado ⭐ NUEVO ├── utils.py # Utilidades comunes ├── constants.py # Constantes y patterns ├── test_all.py # Suite unificada de tests @@ -168,6 +169,13 @@ Sistema modular de **8 reportes interconectados** con navegación por breadcrumb - Auto-scroll a anchors - Sin dependencias externas +### ✅ Sistema de Logging +- Niveles configurables (DEBUG, INFO, WARNING, ERROR, CRITICAL) +- Salida colorizada por nivel de severidad +- Archivo de log opcional con timestamps +- Compatible con formato existente de mensajes +- Útil para debugging y análisis de problemas + --- ## 📈 Estadísticas Actuales @@ -259,6 +267,40 @@ Características: - Puede restaurar backups antes/después de compilar - Muestra errores de compilación detallados +### Logging y Debug ⭐ NUEVO +```bash +# Nivel de debug (muestra mensajes DEBUG adicionales) +./main.py --analyze /path/to/kernel --log-level DEBUG + +# Nivel INFO (default, mensajes informativos) +./main.py --fix --json-input json/checkpatch.json --log-level INFO + +# Nivel WARNING (solo warnings y errores) +./main.py --fix --json-input json/checkpatch.json --log-level WARNING + +# Guardar log en archivo +./main.py --analyze /path/to/kernel --log-file logs/checkpatch.log + +# Desactivar colores (útil para redireccionar output) +./main.py --fix --json-input json/checkpatch.json --no-color + +# Combinación: DEBUG + archivo + sin colores +./main.py --analyze /path/to/kernel --log-level DEBUG --log-file logs/debug.log --no-color +``` + +Niveles de logging disponibles: +- `DEBUG` - Mensajes de debugging detallados (argumentos, archivos procesados, etc.) +- `INFO` - Mensajes informativos (default) - progreso, resultados, archivos modificados +- `WARNING` - Solo advertencias y errores +- `ERROR` - Solo errores críticos +- `CRITICAL` - Solo errores críticos del sistema + +Características: +- Salida colorizada por nivel (rojo=ERROR, amarillo=WARNING, cyan=INFO, gris=DEBUG) +- Archivo de log con timestamps completos (guarda todos los niveles incluido DEBUG) +- Compatible con el formato de mensajes existente `[ANALYZER]`, `[AUTOFIX]`, `[COMPILE]` +- El archivo de log siempre captura nivel DEBUG, independiente del nivel de consola + ### Tests y Análisis ```bash # Suite unificada de tests (12 tests: compilation + fixes + integration) diff --git a/compile.py b/compile.py index 716a0d8..c83d55f 100644 --- a/compile.py +++ b/compile.py @@ -16,6 +16,7 @@ from typing import List, Dict, Tuple, Optional import json import time +import logger class CompilationResult: @@ -61,7 +62,7 @@ def ensure_kernel_configured(kernel_root: Path) -> bool: if config_file.exists(): return True - print("[COMPILE] Kernel not configured. Running 'make defconfig'...") + logger.info("[COMPILE] Kernel not configured. Running 'make defconfig'...") try: result = subprocess.run( ['make', 'defconfig'], @@ -72,14 +73,14 @@ def ensure_kernel_configured(kernel_root: Path) -> bool: ) if result.returncode == 0 and config_file.exists(): - print("[COMPILE] ✓ Kernel configured successfully") + logger.info("[COMPILE] ✓ Kernel configured successfully") return True else: - print(f"[COMPILE] ✗ Failed to configure kernel: {result.stderr[:200]}") + logger.error(f"[COMPILE] ✗ Failed to configure kernel: {result.stderr[:200]}") return False except Exception as e: - print(f"[COMPILE] ✗ Exception while configuring kernel: {e}") + logger.error(f"[COMPILE] ✗ Exception while configuring kernel: {e}") return False @@ -222,7 +223,7 @@ def cleanup_compiled_files(kernel_root: Path, compiled_files: List[Path]): if obj_path.exists(): obj_path.unlink() - print(f"[CLEANUP] Removed: {obj_path.relative_to(kernel_root)}") + logger.debug(f"[CLEANUP] Removed: {obj_path.relative_to(kernel_root)}") # También limpiar posibles archivos auxiliares (.cmd, .d, etc.) cmd_file = obj_path.parent / f".{obj_path.name}.cmd" @@ -234,7 +235,7 @@ def cleanup_compiled_files(kernel_root: Path, compiled_files: List[Path]): d_file.unlink() except Exception as e: - print(f"[CLEANUP WARNING] Could not clean {c_file}: {e}") + logger.warning(f"[CLEANUP WARNING] Could not clean {c_file}: {e}") def compile_modified_files(files: List[Path], kernel_root: Path, diff --git a/logger.py b/logger.py new file mode 100644 index 0000000..b8c1ddc --- /dev/null +++ b/logger.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +""" +logger.py - Sistema de logging unificado para checkpatch + +Proporciona logging con niveles configurables y soporte para archivo de log. +""" + +import logging +import sys +from pathlib import Path + + +# Colores ANSI para la consola +class Colors: + RESET = "\033[0m" + RED = "\033[91m" + GREEN = "\033[92m" + YELLOW = "\033[93m" + BLUE = "\033[94m" + MAGENTA = "\033[95m" + CYAN = "\033[96m" + GRAY = "\033[90m" + + +# Mapeo de niveles a prefijos y colores +LEVEL_CONFIG = { + logging.DEBUG: ("DEBUG", Colors.GRAY), + logging.INFO: ("INFO", Colors.CYAN), + logging.WARNING: ("WARNING", Colors.YELLOW), + logging.ERROR: ("ERROR", Colors.RED), + logging.CRITICAL: ("CRITICAL", Colors.MAGENTA), +} + + +class ColoredFormatter(logging.Formatter): + """Formateador que añade colores a los mensajes de consola.""" + + def __init__(self, use_colors=True): + super().__init__() + self.use_colors = use_colors + + def format(self, record): + if self.use_colors: + level_name, color = LEVEL_CONFIG.get(record.levelno, ("INFO", Colors.CYAN)) + + # Si el mensaje tiene un prefijo como [ANALYZER], [AUTOFIX], etc., respetarlo + msg = record.getMessage() + if msg.startswith('['): + # Mantener el formato existente con prefijo + return f"{color}{msg}{Colors.RESET}" + else: + # Nuevo formato con nivel de log + return f"{color}[{level_name}]{Colors.RESET} {msg}" + else: + return record.getMessage() + + +class CheckpatchLogger: + """Logger singleton para toda la aplicación.""" + + _instance = None + _initialized = False + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self): + if not self._initialized: + self.logger = logging.getLogger('checkpatch') + self.logger.setLevel(logging.DEBUG) # Capturar todos los niveles + self.logger.propagate = False + self._initialized = True + self.file_handler = None + self.console_handler = None + + def setup(self, level=logging.INFO, log_file=None, use_colors=True): + """ + Configura el sistema de logging. + + Args: + level: Nivel de logging (DEBUG, INFO, WARNING, ERROR, CRITICAL) + log_file: Path opcional para archivo de log + use_colors: Si True, usa colores en la salida de consola + """ + # Limpiar handlers existentes + self.logger.handlers.clear() + + # Console handler + self.console_handler = logging.StreamHandler(sys.stdout) + self.console_handler.setLevel(level) + self.console_handler.setFormatter(ColoredFormatter(use_colors=use_colors)) + self.logger.addHandler(self.console_handler) + + # File handler (opcional) + if log_file: + log_path = Path(log_file) + log_path.parent.mkdir(parents=True, exist_ok=True) + + self.file_handler = logging.FileHandler(log_path, mode='a', encoding='utf-8') + self.file_handler.setLevel(logging.DEBUG) # El archivo captura todo + # Sin colores para el archivo + file_formatter = logging.Formatter( + '%(asctime)s - %(levelname)-8s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + self.file_handler.setFormatter(file_formatter) + self.logger.addHandler(self.file_handler) + + def debug(self, msg): + """Log a nivel DEBUG.""" + self.logger.debug(msg) + + def info(self, msg): + """Log a nivel INFO.""" + self.logger.info(msg) + + def warning(self, msg): + """Log a nivel WARNING.""" + self.logger.warning(msg) + + def error(self, msg): + """Log a nivel ERROR.""" + self.logger.error(msg) + + def critical(self, msg): + """Log a nivel CRITICAL.""" + self.logger.critical(msg) + + +# Instancia global singleton +_logger_instance = CheckpatchLogger() + + +def setup_logging(level=logging.INFO, log_file=None, use_colors=True): + """ + Configura el sistema de logging global. + + Args: + level: Nivel de logging (logging.DEBUG, logging.INFO, etc.) + log_file: Path opcional para archivo de log + use_colors: Si True, usa colores en la salida de consola + """ + _logger_instance.setup(level, log_file, use_colors) + + +def debug(msg): + """Log a nivel DEBUG.""" + _logger_instance.debug(msg) + + +def info(msg): + """Log a nivel INFO.""" + _logger_instance.info(msg) + + +def warning(msg): + """Log a nivel WARNING.""" + _logger_instance.warning(msg) + + +def error(msg): + """Log a nivel ERROR.""" + _logger_instance.error(msg) + + +def critical(msg): + """Log a nivel CRITICAL.""" + _logger_instance.critical(msg) + + +def get_level_from_string(level_str): + """ + Convierte string a nivel de logging. + + Args: + level_str: String con el nivel (DEBUG, INFO, WARNING, ERROR, CRITICAL) + + Returns: + logging level constant + """ + level_map = { + 'DEBUG': logging.DEBUG, + 'INFO': logging.INFO, + 'WARNING': logging.WARNING, + 'ERROR': logging.ERROR, + 'CRITICAL': logging.CRITICAL, + } + return level_map.get(level_str.upper(), logging.INFO) diff --git a/main.py b/main.py index 71ca48f..f6beb72 100755 --- a/main.py +++ b/main.py @@ -12,10 +12,14 @@ import argparse import json import sys +import logging from pathlib import Path from concurrent.futures import ThreadPoolExecutor, as_completed import threading +# Sistema de logging +import logger + # Módulos unificados from engine import ( apply_fixes, @@ -54,7 +58,7 @@ def analyze_mode(args): all_files.extend(files) if not all_files: - print(f"[ERROR] No se encontraron archivos con extensiones {args.extensions}") + logger.error(f"[ERROR] No se encontraron archivos con extensiones {args.extensions}") return 1 checkpatch_script = args.checkpatch @@ -78,7 +82,8 @@ def progress_bar(current, total): bar = '#' * filled + ' ' * (bar_len - filled) return f"[{bar}] {percent:.1f}% ({current}/{total})" - print(f"[ANALYZER] Analizando {total} archivos con {args.workers} workers...") + logger.info(f"[ANALYZER] Analizando {total} archivos con {args.workers} workers...") + logger.debug(f"[ANALYZER] Archivos a analizar: {[str(f) for f in all_files[:5]]}{'...' if len(all_files) > 5 else ''}") with ThreadPoolExecutor(max_workers=args.workers) as executor: futures = {executor.submit(analyze_file, f, checkpatch_script, kernel_root): f for f in all_files} @@ -101,9 +106,10 @@ def progress_bar(current, total): completed += 1 if completed % 10 == 0 or completed == total: print(f"\r[ANALYZER] Progreso: {progress_bar(completed, total)}", end="") + logger.debug(f"[ANALYZER] Analizado {file_path}: {len(errors)} errores, {len(warnings)} warnings") except Exception as e: - print(f"\n[ERROR] {file_path}: {e}") + logger.error(f"\n[ERROR] {file_path}: {e}") print() # Nueva línea después de la barra @@ -136,12 +142,12 @@ def progress_bar(current, total): error_count = sum(analysis_data["error_reasons"].values()) warning_count = sum(analysis_data["warning_reasons"].values()) - print(f"[ANALYZER] Errores encontrados: {error_count}") - print(f"[ANALYZER] Warnings encontrados: {warning_count}") - print(f"[ANALYZER] Total encontrados: {error_count + warning_count}") - print(f"[ANALYZER] ✔ Análisis terminado.") - print(f"[ANALYZER] ✔ Informe HTML generado: {html_path}") - print(f"[ANALYZER] ✔ JSON generado: {json_path}") + logger.info(f"[ANALYZER] Errores encontrados: {error_count}") + logger.info(f"[ANALYZER] Warnings encontrados: {warning_count}") + logger.info(f"[ANALYZER] Total encontrados: {error_count + warning_count}") + logger.info(f"[ANALYZER] ✔ Análisis terminado.") + logger.info(f"[ANALYZER] ✔ Informe HTML generado: {html_path}") + logger.info(f"[ANALYZER] ✔ JSON generado: {json_path}") return 0 @@ -151,7 +157,7 @@ def fix_mode(args): json_file = Path(args.json_input) if not json_file.exists(): - print(f"[ERROR] No existe el archivo: {json_file}") + logger.error(f"[ERROR] No existe el archivo: {json_file}") return 1 with open(json_file, "r") as f: @@ -164,7 +170,8 @@ def fix_mode(args): file_filter = Path(args.file).resolve() if args.file else None - print("[AUTOFIX] Procesando archivos...") + logger.info("[AUTOFIX] Procesando archivos...") + logger.debug(f"[AUTOFIX] JSON de entrada: {json_file}, filtro de archivo: {file_filter}") for entry in files_data: file_path = Path(entry["file"]).resolve() @@ -207,7 +214,8 @@ def fix_mode(args): if file_modified: modified_files.add(str(file_path)) - print(f"[AUTOFIX] - {file_path.relative_to(file_path.parent.parent.parent)}") + logger.info(f"[AUTOFIX] - {file_path.relative_to(file_path.parent.parent.parent)}") + logger.debug(f"[AUTOFIX] Modificado archivo: {file_path}") # Calcular estadísticas para resumen errors_fixed = sum(1 for issues in report_data.values() for i in issues.get("error", []) if i.get("fixed")) @@ -217,21 +225,21 @@ def fix_mode(args): # Resumen en consola if errors_fixed + errors_skipped > 0: - print(f"[AUTOFIX] Errores procesados: {errors_fixed + errors_skipped}") - print(f"[AUTOFIX] - Corregidos: {errors_fixed} ({100*errors_fixed/(errors_fixed+errors_skipped):.1f}%)") - print(f"[AUTOFIX] - Saltados : {errors_skipped} ({100*errors_skipped/(errors_fixed+errors_skipped):.1f}%)") + logger.info(f"[AUTOFIX] Errores procesados: {errors_fixed + errors_skipped}") + logger.info(f"[AUTOFIX] - Corregidos: {errors_fixed} ({100*errors_fixed/(errors_fixed+errors_skipped):.1f}%)") + logger.info(f"[AUTOFIX] - Saltados : {errors_skipped} ({100*errors_skipped/(errors_fixed+errors_skipped):.1f}%)") if warnings_fixed + warnings_skipped > 0: - print(f"[AUTOFIX] Warnings procesados: {warnings_fixed + warnings_skipped}") - print(f"[AUTOFIX] - Corregidos: {warnings_fixed} ({100*warnings_fixed/(warnings_fixed+warnings_skipped):.1f}%)") - print(f"[AUTOFIX] - Saltados : {warnings_skipped} ({100*warnings_skipped/(warnings_fixed+warnings_skipped):.1f}%)") + logger.info(f"[AUTOFIX] Warnings procesados: {warnings_fixed + warnings_skipped}") + logger.info(f"[AUTOFIX] - Corregidos: {warnings_fixed} ({100*warnings_fixed/(warnings_fixed+warnings_skipped):.1f}%)") + logger.info(f"[AUTOFIX] - Saltados : {warnings_skipped} ({100*warnings_skipped/(warnings_fixed+warnings_skipped):.1f}%)") total = errors_fixed + warnings_fixed + errors_skipped + warnings_skipped total_fixed = errors_fixed + warnings_fixed if total > 0: - print(f"[AUTOFIX] Total procesados: {total}") - print(f"[AUTOFIX] - Corregidos: {total_fixed} ({100*total_fixed/total:.1f}%)") - print(f"[AUTOFIX] - Saltados : {total - total_fixed} ({100*(total-total_fixed)/total:.1f}%)") + logger.info(f"[AUTOFIX] Total procesados: {total}") + logger.info(f"[AUTOFIX] - Corregidos: {total_fixed} ({100*total_fixed/total:.1f}%)") + logger.info(f"[AUTOFIX] - Saltados : {total - total_fixed} ({100*(total-total_fixed)/total:.1f}%)") # Generar HTML html_path = Path(args.html) @@ -252,9 +260,9 @@ def fix_mode(args): with open(json_out_path, "w", encoding="utf-8") as f: json.dump(report_data, f, indent=2, default=str) - print(f"[AUTOFIX] ✔ Análisis terminado {json_out_path}") - print(f"[AUTOFIX] ✔ Informe HTML generado : {html_path}") - print(f"[AUTOFIX] ✔ JSON generado: {json_out_path}") + logger.info(f"[AUTOFIX] ✔ Análisis terminado {json_out_path}") + logger.info(f"[AUTOFIX] ✔ Informe HTML generado : {html_path}") + logger.info(f"[AUTOFIX] ✔ JSON generado: {json_out_path}") return 0 @@ -264,7 +272,7 @@ def compile_mode(args): json_file = Path(args.json_input) if not json_file.exists(): - print(f"[ERROR] No existe el archivo: {json_file}") + logger.error(f"[ERROR] No existe el archivo: {json_file}") return 1 # Leer archivos modificados del JSON de autofix @@ -291,21 +299,23 @@ def compile_mode(args): modified_files = [Path(entry["file"]) for entry in report_data] if not modified_files: - print("[COMPILE] No se encontraron archivos modificados para compilar") + logger.info("[COMPILE] No se encontraron archivos modificados para compilar") return 0 + logger.debug(f"[COMPILE] Archivos a compilar: {[str(f) for f in modified_files]}") + # Restaurar backups si se solicita if args.restore_before: - print(f"[COMPILE] Restaurando {len(modified_files)} archivos desde backup...") + logger.info(f"[COMPILE] Restaurando {len(modified_files)} archivos desde backup...") restore_backups(modified_files) # Compilar archivos kernel_root = Path(args.kernel_root).resolve() if not kernel_root.exists(): - print(f"[ERROR] Kernel root no encontrado: {kernel_root}") + logger.error(f"[ERROR] Kernel root no encontrado: {kernel_root}") return 1 - print(f"[COMPILE] Kernel root: {kernel_root}") + logger.info(f"[COMPILE] Kernel root: {kernel_root}") results = compile_modified_files( modified_files, kernel_root, @@ -314,7 +324,7 @@ def compile_mode(args): # Restaurar backups después si se solicita if args.restore_after: - print(f"\n[COMPILE] Restaurando {len(modified_files)} archivos desde backup...") + logger.info(f"\n[COMPILE] Restaurando {len(modified_files)} archivos desde backup...") restore_backups(modified_files) # Generar reportes @@ -329,8 +339,8 @@ def compile_mode(args): # Resumen en consola print_summary(results) - print(f"\n[COMPILE] ✓ Informe HTML generado: {html_path}") - print(f"[COMPILE] ✓ JSON generado: {json_path}") + logger.info(f"\n[COMPILE] ✓ Informe HTML generado: {html_path}") + logger.info(f"[COMPILE] ✓ JSON generado: {json_path}") # Retornar 0 si todos compilaron exitosamente, 1 si hubo fallos failed_count = sum(1 for r in results if not r.success) @@ -391,8 +401,30 @@ def main(): parser.add_argument("--html", help="Archivo HTML de salida (default: html/analyzer.html o html/autofix.html)") parser.add_argument("--json-out", help="Archivo JSON de salida (default: json/checkpatch.json o json/fixed.json)") + # Argumentos de logging + logging_group = parser.add_argument_group("Opciones de logging") + logging_group.add_argument("--log-level", + choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], + default="INFO", + help="Nivel de logging (default: INFO)") + logging_group.add_argument("--log-file", + help="Archivo de log opcional (ej: logs/checkpatch.log)") + logging_group.add_argument("--no-color", action="store_true", + help="Desactivar colores en la salida de consola") + args = parser.parse_args() + # Configurar logging + log_level = logger.get_level_from_string(args.log_level) + logger.setup_logging( + level=log_level, + log_file=args.log_file, + use_colors=not args.no_color + ) + + logger.debug(f"[MAIN] Argumentos: {vars(args)}") + logger.debug(f"[MAIN] Nivel de logging: {args.log_level}") + # Validar argumentos según modo if args.analyze: # Configurar rutas automáticamente desde kernel root diff --git a/test_logger.py b/test_logger.py new file mode 100644 index 0000000..88138c3 --- /dev/null +++ b/test_logger.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +""" +Unit tests for the logger module. +""" + +import unittest +import logging +import tempfile +import os +from pathlib import Path +import sys + +# Añadir directorio raíz al path +root_dir = os.path.dirname(os.path.abspath(__file__)) +if root_dir not in sys.path: + sys.path.insert(0, root_dir) + +import logger + + +class TestLogger(unittest.TestCase): + """Tests para el sistema de logging.""" + + def setUp(self): + """Configuración antes de cada test.""" + # Resetear el logger para cada test + logger._logger_instance._initialized = False + logger._logger_instance.__init__() + + def test_logger_singleton(self): + """Verificar que el logger es singleton.""" + logger1 = logger.CheckpatchLogger() + logger2 = logger.CheckpatchLogger() + self.assertIs(logger1, logger2, "Logger should be singleton") + + def test_setup_logging_default(self): + """Verificar configuración default del logging.""" + logger.setup_logging() + # El logger debería estar configurado + self.assertIsNotNone(logger._logger_instance.console_handler) + self.assertIsNone(logger._logger_instance.file_handler) + + def test_setup_logging_with_file(self): + """Verificar configuración con archivo de log.""" + with tempfile.TemporaryDirectory() as tmpdir: + log_file = Path(tmpdir) / "test.log" + logger.setup_logging(log_file=str(log_file)) + + # Escribir mensaje de prueba + logger.info("Test message") + + # Verificar que el archivo existe y contiene el mensaje + self.assertTrue(log_file.exists(), "Log file should exist") + content = log_file.read_text() + self.assertIn("Test message", content) + self.assertIn("INFO", content) + + def test_log_levels(self): + """Verificar que los diferentes niveles funcionan.""" + with tempfile.TemporaryDirectory() as tmpdir: + log_file = Path(tmpdir) / "test.log" + + # Configurar con nivel DEBUG + logger.setup_logging(level=logging.DEBUG, log_file=str(log_file)) + + # Escribir mensajes de diferentes niveles + logger.debug("Debug message") + logger.info("Info message") + logger.warning("Warning message") + logger.error("Error message") + logger.critical("Critical message") + + # Verificar que todos aparecen en el archivo + content = log_file.read_text() + self.assertIn("Debug message", content) + self.assertIn("Info message", content) + self.assertIn("Warning message", content) + self.assertIn("Error message", content) + self.assertIn("Critical message", content) + + def test_get_level_from_string(self): + """Verificar conversión de string a nivel de logging.""" + self.assertEqual(logger.get_level_from_string("DEBUG"), logging.DEBUG) + self.assertEqual(logger.get_level_from_string("INFO"), logging.INFO) + self.assertEqual(logger.get_level_from_string("WARNING"), logging.WARNING) + self.assertEqual(logger.get_level_from_string("ERROR"), logging.ERROR) + self.assertEqual(logger.get_level_from_string("CRITICAL"), logging.CRITICAL) + + # Verificar case-insensitive + self.assertEqual(logger.get_level_from_string("debug"), logging.DEBUG) + self.assertEqual(logger.get_level_from_string("info"), logging.INFO) + + # Verificar default para valores desconocidos + self.assertEqual(logger.get_level_from_string("UNKNOWN"), logging.INFO) + + def test_colored_formatter(self): + """Verificar que el formateador con colores funciona.""" + formatter = logger.ColoredFormatter(use_colors=True) + + # Crear un LogRecord de prueba + record = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="test.py", + lineno=1, + msg="[TEST] Test message", + args=(), + exc_info=None + ) + + formatted = formatter.format(record) + # Debería contener el mensaje + self.assertIn("[TEST] Test message", formatted) + + def test_colored_formatter_no_colors(self): + """Verificar formateador sin colores.""" + formatter = logger.ColoredFormatter(use_colors=False) + + record = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="test.py", + lineno=1, + msg="Test message", + args=(), + exc_info=None + ) + + formatted = formatter.format(record) + self.assertEqual(formatted, "Test message") + # No debería contener códigos ANSI + self.assertNotIn("\033[", formatted) + + def test_log_file_creates_directory(self): + """Verificar que se crean directorios para el archivo de log.""" + with tempfile.TemporaryDirectory() as tmpdir: + log_file = Path(tmpdir) / "subdir" / "logs" / "test.log" + + # El directorio subdir/logs no existe aún + self.assertFalse(log_file.parent.exists()) + + # Configurar logging con este archivo + logger.setup_logging(log_file=str(log_file)) + logger.info("Test") + + # Ahora el directorio y archivo deberían existir + self.assertTrue(log_file.parent.exists()) + self.assertTrue(log_file.exists()) + + +def run_tests(): + """Ejecutar los tests.""" + loader = unittest.TestLoader() + suite = loader.loadTestsFromTestCase(TestLogger) + runner = unittest.TextTestRunner(verbosity=2) + result = runner.run(suite) + return 0 if result.wasSuccessful() else 1 + + +if __name__ == "__main__": + sys.exit(run_tests())