Skip to content

Commit 45b0740

Browse files
authored
Merge pull request #235 from calculquebec/hotfix/logger_permission_error
fix: handle logger permission errors and move default logpath to cwd
2 parents 0f51158 + babec36 commit 45b0740

File tree

2 files changed

+83
-33
lines changed

2 files changed

+83
-33
lines changed

pennylane_calculquebec/logger.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,33 @@
22
import os
33
from pennylane_calculquebec._version import __version__
44

5-
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
5+
DEFAULT_LOG_PATH = os.path.join(
6+
os.getcwd(),
7+
"pennylane_calculquebec.log",
8+
)
69
LOG_PATH = os.environ.get(
7-
"PLCQ_LOG_PATH", os.path.join(ROOT_DIR, "pennylane_calculquebec.log")
10+
"PLCQ_LOG_PATH",
11+
DEFAULT_LOG_PATH,
812
)
913

1014
logger = logging.getLogger("pennylane_calculquebec")
1115
logger.setLevel(logging.INFO)
1216

13-
handler = logging.FileHandler(LOG_PATH, mode="a", encoding="utf-8")
17+
try:
18+
handler = logging.FileHandler(LOG_PATH, mode="a", encoding="utf-8")
19+
20+
except OSError as e:
21+
logging.warning(
22+
"Unable to open log file '%s' for writing due to a %s: %s. "
23+
"Falling back to console logging (StreamHandler).",
24+
LOG_PATH,
25+
type(e).__name__,
26+
e,
27+
)
28+
handler = logging.StreamHandler()
29+
1430
formatter = logging.Formatter(
15-
f"%(asctime)s - The plugin version is {__version__} | Incident: %(message)s",
31+
f"%(asctime)s [%(levelname)s] Version {__version__} | Message: %(message)s",
1632
datefmt="%Y-%m-%d %H:%M:%S",
1733
)
1834
handler.setFormatter(formatter)

tests/test_logger.py

Lines changed: 63 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,74 @@
1+
import importlib
12
import os
2-
import tempfile
33
import logging
4-
from pennylane_calculquebec.logger import logger # Replace with actual import
4+
from unittest.mock import patch
5+
import pytest
6+
from pennylane_calculquebec import logger as logger_module
57

68

7-
def test_logger_creates_virtual_folder_and_logs_message(monkeypatch):
8-
with tempfile.TemporaryDirectory() as tmp_dir:
9-
log_path = os.path.join(tmp_dir, "virtual", "pennylane_calculquebec.log")
9+
@pytest.fixture
10+
def clean_logger():
11+
"""Fixture to ensure logger is in a clean state and handlers are closed."""
12+
# Close existing handlers to release file locks
13+
for h in logger_module.logger.handlers[:]:
14+
logger_module.logger.removeHandler(h)
15+
h.close()
16+
yield
17+
# Cleanup after test
18+
for h in logger_module.logger.handlers[:]:
19+
logger_module.logger.removeHandler(h)
20+
h.close()
1021

11-
# Patch environment variable
12-
monkeypatch.setenv("PLCQ_LOG_PATH", log_path)
1322

14-
# Ensure log directory exists
15-
os.makedirs(os.path.dirname(log_path), exist_ok=True)
23+
def test_logger_write_permission_denied(clean_logger, caplog):
24+
"""Test that logger falls back to StreamHandler if FileHandler fails due to permissions."""
25+
with patch("logging.FileHandler", side_effect=PermissionError("Permission denied")):
26+
importlib.reload(logger_module)
1627

17-
# Remove any existing handlers
18-
for h in logger.handlers[:]:
19-
logger.removeHandler(h)
20-
h.close() # <-- Close existing handlers
28+
handlers = logger_module.logger.handlers
29+
# Check if a StreamHandler is present (and it's not a FileHandler which is a subclass)
30+
assert any(type(h) is logging.StreamHandler for h in handlers)
31+
assert "Unable to open log file" in caplog.text
32+
assert "PermissionError" in caplog.text
2133

22-
# Set up new FileHandler
23-
handler = logging.FileHandler(log_path, mode="a", encoding="utf-8")
24-
formatter = logging.Formatter("Test | %(asctime)s | Incident: %(message)s")
25-
handler.setFormatter(formatter)
26-
logger.addHandler(handler)
2734

28-
# Log test message
29-
test_message = "Simulated error"
30-
logger.error(test_message)
35+
def test_logger_file_not_found(clean_logger, caplog):
36+
"""Test that logger falls back to StreamHandler if FileHandler fails due to file not found."""
37+
with patch("logging.FileHandler", side_effect=FileNotFoundError("File not found")):
38+
importlib.reload(logger_module)
3139

32-
# Verify file created and message written
33-
assert os.path.exists(log_path)
34-
with open(log_path, "r", encoding="utf-8") as f:
35-
contents = f.read()
36-
assert test_message in contents
40+
handlers = logger_module.logger.handlers
41+
# Check if a StreamHandler is present (and it's not a FileHandler which is a subclass)
42+
assert any(type(h) is logging.StreamHandler for h in handlers)
43+
assert "Unable to open log file" in caplog.text
44+
assert "FileNotFoundError" in caplog.text
3745

38-
# Explicitly close the handler to release the file lock
39-
logger.removeHandler(handler)
40-
handler.close()
46+
47+
def test_logger_create_logfile_in_set_env_var(clean_logger, tmp_path):
48+
"""Test that logger uses the path specified in PLCQ_LOG_PATH environment variable."""
49+
log_path = str(tmp_path / "custom_env.log")
50+
51+
with patch.dict(os.environ, {"PLCQ_LOG_PATH": log_path}):
52+
importlib.reload(logger_module)
53+
54+
logger_module.logger.info("test message env var")
55+
56+
assert os.path.exists(log_path)
57+
with open(log_path, "r", encoding="utf-8") as f:
58+
assert "test message env var" in f.read()
59+
60+
61+
def test_logger_create_logfile_in_cwd(clean_logger, tmp_path, monkeypatch):
62+
"""Test that logger defaults to creating a log file in the current working directory."""
63+
# Use monkeypatch to safely change CWD and environment
64+
monkeypatch.chdir(tmp_path)
65+
monkeypatch.delenv("PLCQ_LOG_PATH", raising=False)
66+
67+
importlib.reload(logger_module)
68+
69+
expected_path = os.path.join(os.getcwd(), "pennylane_calculquebec.log")
70+
logger_module.logger.info("test message cwd")
71+
72+
assert os.path.exists(expected_path)
73+
with open(expected_path, "r", encoding="utf-8") as f:
74+
assert "test message cwd" in f.read()

0 commit comments

Comments
 (0)