| |
| |
| |
| |
|
|
| """Tests for logging utilities.""" |
|
|
| """Launch Isaac Sim Simulator first.""" |
|
|
| from isaaclab.app import AppLauncher |
|
|
| |
| simulation_app = AppLauncher(headless=True).app |
|
|
| """Rest everything follows.""" |
|
|
| import logging |
| import os |
| import re |
| import tempfile |
| import time |
|
|
| import pytest |
|
|
| from isaaclab.utils.logger import ColoredFormatter, RateLimitFilter, configure_logging |
|
|
|
|
| |
| @pytest.fixture |
| def formatter(): |
| """Fixture providing a ColoredFormatter instance.""" |
| return ColoredFormatter("%(levelname)s: %(message)s") |
|
|
|
|
| @pytest.fixture |
| def test_message(): |
| """Fixture providing a test message string.""" |
| return "Test message" |
|
|
|
|
| @pytest.fixture |
| def rate_limit_filter(): |
| """Fixture providing a RateLimitFilter instance with 2 second interval.""" |
| return RateLimitFilter(interval_seconds=2) |
|
|
|
|
| """ |
| Tests for the ColoredFormatter class. |
| """ |
|
|
|
|
| def test_info_formatting(formatter, test_message): |
| """Test INFO level message formatting.""" |
| record = logging.LogRecord( |
| name="test", |
| level=logging.INFO, |
| pathname="test.py", |
| lineno=1, |
| msg=test_message, |
| args=(), |
| exc_info=None, |
| ) |
| formatted = formatter.format(record) |
|
|
| |
| assert "\033[0m" in formatted |
| assert test_message in formatted |
| assert "INFO" in formatted |
|
|
|
|
| def test_debug_formatting(formatter, test_message): |
| """Test DEBUG level message formatting.""" |
| record = logging.LogRecord( |
| name="test", |
| level=logging.DEBUG, |
| pathname="test.py", |
| lineno=1, |
| msg=test_message, |
| args=(), |
| exc_info=None, |
| ) |
| formatted = formatter.format(record) |
|
|
| |
| assert "\033[0m" in formatted |
| assert test_message in formatted |
| assert "DEBUG" in formatted |
|
|
|
|
| def test_warning_formatting(formatter, test_message): |
| """Test WARNING level message formatting.""" |
| record = logging.LogRecord( |
| name="test", |
| level=logging.WARNING, |
| pathname="test.py", |
| lineno=1, |
| msg=test_message, |
| args=(), |
| exc_info=None, |
| ) |
| formatted = formatter.format(record) |
|
|
| |
| assert "\033[33m" in formatted |
| assert test_message in formatted |
| assert "WARNING" in formatted |
| |
| assert formatted.endswith("\033[0m") |
|
|
|
|
| def test_error_formatting(formatter, test_message): |
| """Test ERROR level message formatting.""" |
| record = logging.LogRecord( |
| name="test", |
| level=logging.ERROR, |
| pathname="test.py", |
| lineno=1, |
| msg=test_message, |
| args=(), |
| exc_info=None, |
| ) |
| formatted = formatter.format(record) |
|
|
| |
| assert "\033[31m" in formatted |
| assert test_message in formatted |
| assert "ERROR" in formatted |
| |
| assert formatted.endswith("\033[0m") |
|
|
|
|
| def test_critical_formatting(formatter, test_message): |
| """Test CRITICAL level message formatting.""" |
| record = logging.LogRecord( |
| name="test", |
| level=logging.CRITICAL, |
| pathname="test.py", |
| lineno=1, |
| msg=test_message, |
| args=(), |
| exc_info=None, |
| ) |
| formatted = formatter.format(record) |
|
|
| |
| assert "\033[1;31m" in formatted |
| assert test_message in formatted |
| assert "CRITICAL" in formatted |
| |
| assert formatted.endswith("\033[0m") |
|
|
|
|
| def test_color_codes_are_ansi(): |
| """Test that color codes are valid ANSI escape sequences.""" |
| |
| for level_name, color_code in ColoredFormatter.COLORS.items(): |
| |
| assert re.match(r"\033\[[\d;]+m", color_code), f"Invalid ANSI color code for {level_name}" |
|
|
| |
| assert re.match(r"\033\[[\d;]+m", ColoredFormatter.RESET), "Invalid ANSI reset code" |
|
|
|
|
| def test_custom_format_string(test_message): |
| """Test that custom format strings work correctly.""" |
| custom_formatter = ColoredFormatter("%(name)s - %(levelname)s - %(message)s") |
| record = logging.LogRecord( |
| name="custom.logger", |
| level=logging.WARNING, |
| pathname="test.py", |
| lineno=1, |
| msg=test_message, |
| args=(), |
| exc_info=None, |
| ) |
| formatted = custom_formatter.format(record) |
|
|
| assert "custom.logger" in formatted |
| assert "WARNING" in formatted |
| assert test_message in formatted |
| assert "\033[33m" in formatted |
|
|
|
|
| """ |
| Tests for the RateLimitFilter class. |
| """ |
|
|
|
|
| def test_non_warning_messages_pass_through(rate_limit_filter): |
| """Test that non-WARNING messages always pass through the filter.""" |
| |
| info_record = logging.LogRecord( |
| name="test", |
| level=logging.INFO, |
| pathname="test.py", |
| lineno=1, |
| msg="Info message", |
| args=(), |
| exc_info=None, |
| ) |
| assert rate_limit_filter.filter(info_record) is True |
|
|
| |
| error_record = logging.LogRecord( |
| name="test", |
| level=logging.ERROR, |
| pathname="test.py", |
| lineno=1, |
| msg="Error message", |
| args=(), |
| exc_info=None, |
| ) |
| assert rate_limit_filter.filter(error_record) is True |
|
|
| |
| debug_record = logging.LogRecord( |
| name="test", |
| level=logging.DEBUG, |
| pathname="test.py", |
| lineno=1, |
| msg="Debug message", |
| args=(), |
| exc_info=None, |
| ) |
| assert rate_limit_filter.filter(debug_record) is True |
|
|
|
|
| def test_first_warning_passes(rate_limit_filter): |
| """Test that the first WARNING message passes through.""" |
| record = logging.LogRecord( |
| name="test", |
| level=logging.WARNING, |
| pathname="test.py", |
| lineno=1, |
| msg="First warning", |
| args=(), |
| exc_info=None, |
| ) |
| assert rate_limit_filter.filter(record) is True |
|
|
|
|
| def test_duplicate_warning_within_interval_blocked(rate_limit_filter): |
| """Test that duplicate WARNING messages within interval are blocked.""" |
| message = "Duplicate warning" |
|
|
| |
| record1 = logging.LogRecord( |
| name="test", |
| level=logging.WARNING, |
| pathname="test.py", |
| lineno=1, |
| msg=message, |
| args=(), |
| exc_info=None, |
| ) |
| assert rate_limit_filter.filter(record1) is True |
|
|
| |
| record2 = logging.LogRecord( |
| name="test", |
| level=logging.WARNING, |
| pathname="test.py", |
| lineno=2, |
| msg=message, |
| args=(), |
| exc_info=None, |
| ) |
| assert rate_limit_filter.filter(record2) is False |
|
|
|
|
| def test_warning_after_interval_passes(): |
| """Test that WARNING messages pass after the rate limit interval.""" |
| message = "Rate limited warning" |
| filter_short = RateLimitFilter(interval_seconds=1) |
|
|
| |
| record1 = logging.LogRecord( |
| name="test", |
| level=logging.WARNING, |
| pathname="test.py", |
| lineno=1, |
| msg=message, |
| args=(), |
| exc_info=None, |
| ) |
| assert filter_short.filter(record1) is True |
|
|
| |
| record2 = logging.LogRecord( |
| name="test", |
| level=logging.WARNING, |
| pathname="test.py", |
| lineno=2, |
| msg=message, |
| args=(), |
| exc_info=None, |
| ) |
| assert filter_short.filter(record2) is False |
|
|
| |
| time.sleep(1.1) |
|
|
| |
| record3 = logging.LogRecord( |
| name="test", |
| level=logging.WARNING, |
| pathname="test.py", |
| lineno=3, |
| msg=message, |
| args=(), |
| exc_info=None, |
| ) |
| assert filter_short.filter(record3) is True |
|
|
|
|
| def test_different_warnings_not_rate_limited(rate_limit_filter): |
| """Test that different WARNING messages are not rate limited together.""" |
| |
| record1 = logging.LogRecord( |
| name="test", |
| level=logging.WARNING, |
| pathname="test.py", |
| lineno=1, |
| msg="Warning A", |
| args=(), |
| exc_info=None, |
| ) |
| assert rate_limit_filter.filter(record1) is True |
|
|
| |
| record2 = logging.LogRecord( |
| name="test", |
| level=logging.WARNING, |
| pathname="test.py", |
| lineno=2, |
| msg="Warning B", |
| args=(), |
| exc_info=None, |
| ) |
| assert rate_limit_filter.filter(record2) is True |
|
|
|
|
| def test_custom_interval(): |
| """Test that custom interval seconds work correctly.""" |
| custom_filter = RateLimitFilter(interval_seconds=1) |
| assert custom_filter.interval == 1 |
|
|
| long_filter = RateLimitFilter(interval_seconds=10) |
| assert long_filter.interval == 10 |
|
|
|
|
| def test_last_emitted_tracking(rate_limit_filter): |
| """Test that the filter correctly tracks last emission times.""" |
| message1 = "Message 1" |
| message2 = "Message 2" |
|
|
| |
| record1 = logging.LogRecord( |
| name="test", |
| level=logging.WARNING, |
| pathname="test.py", |
| lineno=1, |
| msg=message1, |
| args=(), |
| exc_info=None, |
| ) |
| rate_limit_filter.filter(record1) |
|
|
| |
| assert message1 in rate_limit_filter.last_emitted |
|
|
| |
| record2 = logging.LogRecord( |
| name="test", |
| level=logging.WARNING, |
| pathname="test.py", |
| lineno=2, |
| msg=message2, |
| args=(), |
| exc_info=None, |
| ) |
| rate_limit_filter.filter(record2) |
|
|
| |
| assert message1 in rate_limit_filter.last_emitted |
| assert message2 in rate_limit_filter.last_emitted |
|
|
| |
| assert rate_limit_filter.last_emitted[message1] <= rate_limit_filter.last_emitted[message2] |
|
|
|
|
| def test_formatted_message_warnings(rate_limit_filter): |
| """Test rate limiting with formatted WARNING messages.""" |
| |
| record1 = logging.LogRecord( |
| name="test", |
| level=logging.WARNING, |
| pathname="test.py", |
| lineno=1, |
| msg="Warning: value=%d", |
| args=(42,), |
| exc_info=None, |
| ) |
| assert rate_limit_filter.filter(record1) is True |
|
|
| |
| record2 = logging.LogRecord( |
| name="test", |
| level=logging.WARNING, |
| pathname="test.py", |
| lineno=2, |
| msg="Warning: value=%d", |
| args=(42,), |
| exc_info=None, |
| ) |
| assert rate_limit_filter.filter(record2) is False |
|
|
| |
| record3 = logging.LogRecord( |
| name="test", |
| level=logging.WARNING, |
| pathname="test.py", |
| lineno=3, |
| msg="Warning: value=%d", |
| args=(99,), |
| exc_info=None, |
| ) |
| assert rate_limit_filter.filter(record3) is True |
|
|
|
|
| """ |
| Integration Tests. |
| |
| Tests that the filter and formatter work together in a logger. |
| """ |
|
|
|
|
| def test_filter_and_formatter_together(): |
| """Test that filter and formatter work together in a logger.""" |
| |
| test_logger = logging.getLogger("test_integration") |
| test_logger.setLevel(logging.DEBUG) |
|
|
| |
| test_logger.handlers.clear() |
|
|
| |
| handler = logging.StreamHandler() |
| handler.setFormatter(ColoredFormatter("%(levelname)s: %(message)s")) |
|
|
| |
| rate_filter = RateLimitFilter(interval_seconds=1) |
| handler.addFilter(rate_filter) |
|
|
| test_logger.addHandler(handler) |
|
|
| |
| assert len(test_logger.handlers) == 1 |
| assert isinstance(test_logger.handlers[0].formatter, ColoredFormatter) |
|
|
| |
| test_logger.handlers.clear() |
|
|
|
|
| def test_default_initialization(): |
| """Test that classes can be initialized with default parameters.""" |
| |
| formatter = ColoredFormatter() |
| assert formatter is not None |
|
|
| |
| filter_obj = RateLimitFilter() |
| assert filter_obj.interval == 5 |
|
|
|
|
| """ |
| Tests for the configure_logging function. |
| """ |
|
|
|
|
| def test_configure_logging_basic(): |
| """Test basic configure_logging functionality without file logging.""" |
| |
| logger = configure_logging(logging_level="INFO", save_logs_to_file=False) |
|
|
| |
| assert logger is not None |
| assert logger is logging.getLogger() |
| |
| assert logger.level == logging.DEBUG |
|
|
| |
| assert len(logger.handlers) == 1 |
|
|
| |
| stream_handler = logger.handlers[0] |
| assert isinstance(stream_handler, logging.StreamHandler) |
| assert isinstance(stream_handler.formatter, ColoredFormatter) |
| assert stream_handler.level == logging.INFO |
|
|
| |
| assert len(stream_handler.filters) > 0 |
| rate_filter = stream_handler.filters[0] |
| assert isinstance(rate_filter, RateLimitFilter) |
| assert rate_filter.interval == 5 |
|
|
|
|
| def test_configure_logging_with_file(): |
| """Test configure_logging with file logging enabled.""" |
| |
| with tempfile.TemporaryDirectory() as temp_dir: |
| logger = configure_logging(logging_level="DEBUG", save_logs_to_file=True, log_dir=temp_dir) |
|
|
| |
| assert logger is not None |
| |
| assert logger.level == logging.DEBUG |
|
|
| |
| assert len(logger.handlers) == 2 |
|
|
| |
| stream_handler = logger.handlers[0] |
| assert isinstance(stream_handler, logging.StreamHandler) |
| assert isinstance(stream_handler.formatter, ColoredFormatter) |
| assert stream_handler.level == logging.DEBUG |
|
|
| |
| file_handler = logger.handlers[1] |
| assert isinstance(file_handler, logging.FileHandler) |
| assert file_handler.level == logging.DEBUG |
|
|
| |
| log_files = [f for f in os.listdir(temp_dir) if f.startswith("isaaclab_")] |
| assert len(log_files) == 1 |
|
|
|
|
| def test_configure_logging_levels(): |
| """Test configure_logging with different logging levels.""" |
| from typing import Literal |
|
|
| levels: list[Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]] = [ |
| "DEBUG", |
| "INFO", |
| "WARNING", |
| "ERROR", |
| "CRITICAL", |
| ] |
| level_values = { |
| "DEBUG": logging.DEBUG, |
| "INFO": logging.INFO, |
| "WARNING": logging.WARNING, |
| "ERROR": logging.ERROR, |
| "CRITICAL": logging.CRITICAL, |
| } |
|
|
| for level_str in levels: |
| logger = configure_logging(logging_level=level_str, save_logs_to_file=False) |
| |
| assert logger.level == logging.DEBUG |
| |
| assert logger.handlers[0].level == level_values[level_str] |
|
|
|
|
| def test_configure_logging_removes_existing_handlers(): |
| """Test that configure_logging removes existing handlers.""" |
| |
| root_logger = logging.getLogger() |
| dummy_handler = logging.StreamHandler() |
| root_logger.addHandler(dummy_handler) |
|
|
| initial_handler_count = len(root_logger.handlers) |
| assert initial_handler_count > 0 |
|
|
| |
| logger = configure_logging(logging_level="INFO", save_logs_to_file=False) |
|
|
| |
| assert len(logger.handlers) == 1 |
| assert dummy_handler not in logger.handlers |
|
|
|
|
| def test_configure_logging_default_log_dir(): |
| """Test configure_logging uses temp directory when log_dir is None.""" |
|
|
| logger = configure_logging(logging_level="INFO", save_logs_to_file=True, log_dir=None) |
|
|
| |
| assert logger.level == logging.DEBUG |
|
|
| |
| assert len(logger.handlers) == 2 |
| file_handler = logger.handlers[1] |
| assert isinstance(file_handler, logging.FileHandler) |
|
|
| |
| log_file_path = file_handler.baseFilename |
| assert os.path.dirname(log_file_path) == os.path.join(tempfile.gettempdir(), "isaaclab", "logs") |
| assert os.path.basename(log_file_path).startswith("isaaclab_") |
|
|
| |
| if os.path.exists(log_file_path): |
| os.remove(log_file_path) |
|
|
|
|
| def test_configure_logging_custom_log_dir(): |
| """Test configure_logging with custom log directory.""" |
| with tempfile.TemporaryDirectory() as temp_dir: |
| custom_log_dir = os.path.join(temp_dir, "custom_logs") |
|
|
| logger = configure_logging(logging_level="INFO", save_logs_to_file=True, log_dir=custom_log_dir) |
|
|
| |
| assert os.path.exists(custom_log_dir) |
| assert os.path.isdir(custom_log_dir) |
|
|
| |
| assert logger.level == logging.DEBUG |
|
|
| |
| file_handler = logger.handlers[1] |
| assert isinstance(file_handler, logging.FileHandler) |
| log_file_path = file_handler.baseFilename |
| assert os.path.dirname(log_file_path) == custom_log_dir |
|
|
|
|
| def test_configure_logging_log_file_format(): |
| """Test that log file has correct timestamp format.""" |
| with tempfile.TemporaryDirectory() as temp_dir: |
| logger = configure_logging(logging_level="INFO", save_logs_to_file=True, log_dir=temp_dir) |
|
|
| |
| assert logger.level == logging.DEBUG |
|
|
| |
| file_handler = logger.handlers[1] |
| assert isinstance(file_handler, logging.FileHandler) |
| log_file_path = file_handler.baseFilename |
| log_filename = os.path.basename(log_file_path) |
|
|
| |
| pattern = r"isaaclab_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.log" |
| assert re.match(pattern, log_filename), f"Log filename {log_filename} doesn't match expected pattern" |
|
|
|
|
| def test_configure_logging_file_formatter(): |
| """Test that file handler has more detailed formatter than stream handler.""" |
| with tempfile.TemporaryDirectory() as temp_dir: |
| logger = configure_logging(logging_level="INFO", save_logs_to_file=True, log_dir=temp_dir) |
|
|
| |
| assert logger.level == logging.DEBUG |
|
|
| stream_handler = logger.handlers[0] |
| file_handler = logger.handlers[1] |
|
|
| |
| assert stream_handler.formatter is not None |
| assert isinstance(stream_handler.formatter, ColoredFormatter) |
| stream_format = stream_handler.formatter._fmt |
| assert stream_format is not None |
| assert "%(asctime)s" in stream_format |
| assert "%(filename)s" in stream_format |
|
|
| |
| assert file_handler.formatter is not None |
| assert isinstance(file_handler.formatter, logging.Formatter) |
| file_format = file_handler.formatter._fmt |
| assert file_format is not None |
| assert "%(asctime)s" in file_format |
| assert "%(lineno)d" in file_format |
|
|
| |
| assert file_handler.level == logging.DEBUG |
|
|
|
|
| def test_configure_logging_multiple_calls(): |
| """Test that multiple configure_logging calls properly cleanup.""" |
| |
| logger1 = configure_logging(logging_level="INFO", save_logs_to_file=False) |
| handler_count_1 = len(logger1.handlers) |
|
|
| |
| logger2 = configure_logging(logging_level="DEBUG", save_logs_to_file=False) |
| handler_count_2 = len(logger2.handlers) |
|
|
| |
| assert logger1 is logger2 |
|
|
| |
| assert handler_count_1 == handler_count_2 == 1 |
|
|
|
|
| def test_configure_logging_actual_logging(): |
| """Test that logger actually logs messages correctly.""" |
| import io |
|
|
| |
| captured_output = io.StringIO() |
|
|
| |
| logger = configure_logging(logging_level="INFO", save_logs_to_file=False) |
|
|
| |
| stream_handler = logger.handlers[0] |
| assert isinstance(stream_handler, logging.StreamHandler) |
| original_stream = stream_handler.stream |
| stream_handler.stream = captured_output |
|
|
| |
| test_logger = logging.getLogger("test_module") |
| test_logger.info("Test info message") |
| test_logger.warning("Test warning message") |
| test_logger.debug("Test debug message") |
|
|
| |
| stream_handler.stream = original_stream |
|
|
| |
| output = captured_output.getvalue() |
| assert "Test info message" in output |
| assert "Test warning message" in output |
| assert "Test debug message" not in output |
| assert "INFO" in output |
| assert "WARNING" in output |
|
|