#
# Copyright (c) 2025 Leland Stanford Junior University
# Copyright (c) 2025 The Regents of the University of California
#
# This file is part of pelicun.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors
# may be used to endorse or promote products derived from this software without
# specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# You should have received a copy of the BSD 3-Clause License along with
# pelicun. If not, see <http://www.opensource.org/licenses/>.
#
# Contributors:
# Adam Zsarnóczay
"""These are unit tests for the CLI logging functionality of pelicun."""
from __future__ import annotations
import logging
import os
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from pelicun.cli import setup_dlml_logging
[docs]
def test_setup_dlml_logging_stdout_only() -> None:
"""Test setup_dlml_logging with stdout logging only."""
# Clear any existing handlers
logger = logging.getLogger('pelicun.dlml')
logger.handlers.clear()
# Setup logging without file
setup_dlml_logging(log_file=None)
# Verify logger configuration
assert logger.level == logging.INFO
assert len(logger.handlers) == 1
# Verify handler is StreamHandler
handler = logger.handlers[0]
assert isinstance(handler, logging.StreamHandler)
# Verify formatter
formatter = handler.formatter
assert formatter._fmt == '%(message)s'
# Clean up
logger.handlers.clear()
[docs]
def test_setup_dlml_logging_with_file() -> None:
"""Test setup_dlml_logging with file logging."""
# Clear any existing handlers
logger = logging.getLogger('pelicun.dlml')
logger.handlers.clear()
# Create temporary file
with tempfile.NamedTemporaryFile(delete=False, suffix='.log') as temp_file:
temp_filename = temp_file.name
try:
# Setup logging with file
with patch('builtins.print') as mock_print:
setup_dlml_logging(log_file=temp_filename)
# Verify logger configuration
assert logger.level == logging.INFO
assert len(logger.handlers) == 2 # stdout + file
# Verify handlers
handlers = logger.handlers
stream_handler = next(
h for h in handlers if isinstance(h, logging.StreamHandler)
)
file_handler = next(
h for h in handlers if isinstance(h, logging.FileHandler)
)
# Verify formatters
assert stream_handler.formatter._fmt == '%(message)s'
assert (
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
in file_handler.formatter._fmt
)
# Verify print was called to announce log file
mock_print.assert_called_once_with(f'Logging to file: {temp_filename}')
finally:
# Clean up
for handler in logger.handlers:
if isinstance(handler, logging.FileHandler):
handler.close()
logger.handlers.clear()
temp_path = Path(temp_filename)
if temp_path.exists():
temp_path.unlink()
[docs]
def test_setup_dlml_logging_with_timestamp_file() -> None:
"""Test setup_dlml_logging with automatic timestamp file creation."""
# Clear any existing handlers
logger = logging.getLogger('pelicun.dlml')
logger.handlers.clear()
with patch('builtins.print') as mock_print, patch(
'pelicun.cli.datetime'
) as mock_datetime:
# Mock datetime to return predictable timestamp
mock_datetime.now.return_value.strftime.return_value = '2025-08-13_22-30-00'
# Setup logging with True (auto-timestamp)
setup_dlml_logging(log_file=True)
# Verify logger configuration
assert logger.level == logging.INFO
assert len(logger.handlers) == 2 # stdout + file
# Verify print was called with timestamped filename
expected_filename = 'dlml_update_2025-08-13_22-30-00.log'
mock_print.assert_called_once_with(f'Logging to file: {expected_filename}')
# Clean up
for handler in logger.handlers:
if isinstance(handler, logging.FileHandler):
handler.close()
logger.handlers.clear()
expected_path = Path(expected_filename)
if expected_path.exists():
expected_path.unlink()
[docs]
def test_setup_dlml_logging_no_duplicate_handlers() -> None:
"""Test that setup_dlml_logging doesn't add duplicate handlers."""
# Clear any existing handlers
logger = logging.getLogger('pelicun.dlml')
logger.handlers.clear()
# Setup logging twice
setup_dlml_logging(log_file=None)
initial_handler_count = len(logger.handlers)
setup_dlml_logging(log_file=None)
final_handler_count = len(logger.handlers)
# Should not add duplicate handlers
assert initial_handler_count == final_handler_count == 1
# Clean up
logger.handlers.clear()
[docs]
def test_setup_dlml_logging_file_creation() -> None:
"""Test that file logging actually creates and writes to the log file."""
# Clear any existing handlers
logger = logging.getLogger('pelicun.dlml')
logger.handlers.clear()
# Create temporary file
with tempfile.NamedTemporaryFile(delete=False, suffix='.log') as temp_file:
temp_filename = temp_file.name
try:
# Setup logging with file
setup_dlml_logging(log_file=temp_filename)
# Log a test message
logger.info('Test message for file logging')
# Verify file was created and contains the message
temp_path = Path(temp_filename)
assert temp_path.exists()
with temp_path.open('r') as f:
content = f.read()
assert 'Test message for file logging' in content
assert 'pelicun.dlml' in content
assert 'INFO' in content
finally:
# Clean up
for handler in logger.handlers:
if isinstance(handler, logging.FileHandler):
handler.close()
logger.handlers.clear()
if temp_path.exists():
temp_path.unlink()
[docs]
def test_cli_integration_with_logging() -> None:
"""Test CLI integration with logging setup."""
import subprocess # noqa: PLC0415, S404
# Test that CLI help includes the --log option
result = subprocess.run( # noqa: S603
['pelicun', 'dlml', '--help'], # noqa: S607
capture_output=True,
text=True,
cwd=Path(__file__).parent.parent.parent, # Use project root dynamically
check=False,
)
assert result.returncode == 0
assert '--log [LOGFILE]' in result.stdout
assert 'Save detailed log to specified file' in result.stdout
assert 'creates dlml_update_TIMESTAMP.log' in result.stdout