Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions tests/logging/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
import pytest

from daqpytools.logging.exceptions import (
ERSEnvError,
ERSInitError,
LoggerConfigurationError,
LoggerHandlerError,
LoggerSetupError,
ProtobufFormatError,
)


Expand All @@ -23,3 +27,25 @@ def test_exceptions():
assert str(exc_info.value) == (
"Constructing test_logger failed as: \nThe test made me do it :("
)


def test_configuration_and_ers_related_exceptions() -> None:
config_path = "tests/logging/log_format.ini"
with pytest.raises(LoggerConfigurationError) as exc_info:
raise LoggerConfigurationError(config_path, "bad section")
assert f"Configuration file '{config_path}'" in str(exc_info.value)
assert "bad section" in str(exc_info.value)

with pytest.raises(ERSEnvError) as exc_info:
raise ERSEnvError("DUNEDAQ_ERS_ERROR")
assert str(exc_info.value) == "The environment variable DUNEDAQ_ERS_ERROR is empty"

with pytest.raises(ERSInitError) as exc_info:
raise ERSInitError("host:30092", "ers_stream")
assert "address='host:30092'" in str(exc_info.value)
assert "topic='ers_stream'" in str(exc_info.value)

with pytest.raises(ProtobufFormatError) as exc_info:
raise ProtobufFormatError("protobufstream(bad)")
assert "protobufstream URLs must be formatted (url:port)." in str(exc_info.value)
assert "protobufstream(bad)" in str(exc_info.value)
134 changes: 134 additions & 0 deletions tests/logging/test_filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import logging
import uuid
from collections.abc import Iterator
from unittest.mock import MagicMock

import pytest

from daqpytools.logging.filters import (
HandleIDFilter,
ThrottleFilter,
add_filter,
add_throttle_filter,
get_filter_spec,
)
from daqpytools.logging.handlerconf import HandlerType


def _record(level: int = logging.INFO) -> logging.LogRecord:
return logging.LogRecord(
name="test.filters",
level=level,
pathname="<test>",
lineno=42,
msg="message",
args=(),
exc_info=None,
)


@pytest.fixture
def clean_logger() -> Iterator[logging.Logger]:
name = f"test.filters.{uuid.uuid4()}"
logger = logging.getLogger(name)
logger.handlers = []
logger.filters = []
logger.propagate = False
logger.setLevel(logging.DEBUG)
yield logger
for handler in logger.handlers[:]:
logger.removeHandler(handler)
logger.filters = []
logging.root.manager.loggerDict.pop(name, None)


def test_get_filter_spec_registry_lookup() -> None:
assert get_filter_spec(HandlerType.Throttle) is not None
assert get_filter_spec(HandlerType.Rich) is None


def test_add_filter_uses_default_fallback_from_spec(
clean_logger: logging.Logger,
) -> None:
add_filter(clean_logger, HandlerType.Throttle, fallback_handlers=None)

assert len(clean_logger.filters) == 1
logger_filter = clean_logger.filters[0]
assert isinstance(logger_filter, ThrottleFilter)
assert logger_filter.fallback_handlers == {HandlerType.Throttle}


def test_add_filter_uses_explicit_fallback_and_extras(
clean_logger: logging.Logger,
) -> None:
add_filter(
clean_logger,
HandlerType.Throttle,
fallback_handlers={HandlerType.Unknown},
initial_treshold=7,
time_limit=11,
)

logger_filter = clean_logger.filters[0]
assert isinstance(logger_filter, ThrottleFilter)
assert logger_filter.fallback_handlers == {HandlerType.Unknown}
assert logger_filter.initial_threshold == 7
assert logger_filter.time_limit == 11


def test_add_throttle_filter_delegates_to_add_filter(
clean_logger: logging.Logger,
monkeypatch: pytest.MonkeyPatch,
) -> None:
add_filter_mock = MagicMock()
monkeypatch.setattr("daqpytools.logging.filters.add_filter", add_filter_mock)

add_throttle_filter(clean_logger, fallback_handlers={HandlerType.Throttle})

add_filter_mock.assert_called_once_with(
clean_logger,
HandlerType.Throttle,
{HandlerType.Throttle},
)


def test_handleid_filter_matches_with_fallback_when_record_handlers_missing() -> None:
logger_filter = HandleIDFilter(
handler_id=HandlerType.Rich,
fallback_handlers={HandlerType.Rich},
)

assert logger_filter.filter(_record()) is True


def test_handleid_filter_rejects_when_no_allowed_handlers() -> None:
logger_filter = HandleIDFilter(
handler_id=HandlerType.Rich,
fallback_handlers={HandlerType.Rich},
)
record = _record()
record.handlers = None

assert logger_filter.filter(record) is False


def test_throttle_filter_passthrough_when_throttle_not_allowed() -> None:
logger_filter = ThrottleFilter(
fallback_handlers={HandlerType.Rich},
initial_threshold=1,
time_limit=60,
)

assert logger_filter.filter(_record(level=logging.ERROR)) is True


def test_throttle_filter_suppresses_after_initial_threshold() -> None:
logger_filter = ThrottleFilter(
fallback_handlers={HandlerType.Throttle},
initial_threshold=1,
time_limit=60,
)
record = _record(level=logging.ERROR)

assert logger_filter.filter(record) is True
assert logger_filter.filter(record) is False
215 changes: 215 additions & 0 deletions tests/logging/test_handlerconf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import logging
import uuid
from typing import ClassVar
from unittest.mock import MagicMock

import pytest

from daqpytools.apps import logging_demonstrator as demo
from daqpytools.logging.exceptions import ERSEnvError, ProtobufFormatError
from daqpytools.logging.handlerconf import (
ERSPyLogHandlerConf,
HandlerType,
LogHandlerConf,
ProtobufConf,
StreamType,
)
from daqpytools.logging.levels import level_to_ers_var


def test_handlertype_from_string_case_insensitive() -> None:
assert HandlerType.from_string("RiCh") == HandlerType.Rich


def test_handlertype_from_string_unknown_returns_none() -> None:
assert HandlerType.from_string("definitely_unknown") is None


def test_protobufconf_get_string_formats_url_port() -> None:
conf = ProtobufConf(url="host", port=1234)
assert conf.get_string() == "host:1234"


def test_loghandlerconf_ers_property_raises_before_init() -> None:
conf = LogHandlerConf(init_ers=False)
with pytest.raises(AttributeError, match="ERS stream not initialised"):
_ = conf.ERS


def test_loghandlerconf_init_ers_stream_sets_structure(
monkeypatch: pytest.MonkeyPatch,
) -> None:
fake_oks = {"DUNEDAQ_ERS_ERROR": ERSPyLogHandlerConf(handlers=[HandlerType.Rich])}
monkeypatch.setattr(
LogHandlerConf, "_get_oks_conf", staticmethod(lambda: fake_oks)
)

conf = LogHandlerConf(init_ers=False)
conf.init_ers_stream()

assert conf.ERS["ers_handlers"] == fake_oks
assert conf.ERS["stream"] == StreamType.ERS


def test_loghandlerconf_post_init_calls_init_when_flag_true(
monkeypatch: pytest.MonkeyPatch,
) -> None:
fake_oks = {"DUNEDAQ_ERS_ERROR": ERSPyLogHandlerConf(handlers=[HandlerType.Rich])}
monkeypatch.setattr(LogHandlerConf, "_get_oks_conf", staticmethod(lambda: fake_oks))

conf = LogHandlerConf(init_ers=True)
assert conf.ERS["ers_handlers"] == fake_oks


def test_get_base_returns_copy_not_original_reference() -> None:
base_one = LogHandlerConf.get_base()
base_two = LogHandlerConf.get_base()

base_one.add(HandlerType.Unknown)

assert HandlerType.Unknown in base_one
assert HandlerType.Unknown not in base_two


def test_convert_str_to_handlertype_ignores_erstrace() -> None:
handler, protobuf_conf = LogHandlerConf._convert_str_to_handlertype("erstrace")
assert handler is None
assert protobuf_conf is None


def test_convert_str_to_handlertype_regular_handler() -> None:
handler, protobuf_conf = LogHandlerConf._convert_str_to_handlertype("throttle")
assert handler == HandlerType.Throttle
assert protobuf_conf is None


def test_convert_str_to_handlertype_parses_protobuf_with_url_port() -> None:
handler, protobuf_conf = LogHandlerConf._convert_str_to_handlertype(
"protobufstream(monkafka.cern.ch:30092)"
)
assert handler == HandlerType.Protobufstream
assert protobuf_conf == ProtobufConf(url="monkafka.cern.ch", port=30092)


def test_convert_str_to_handlertype_invalid_protobuf_format_raises() -> None:
with pytest.raises(ProtobufFormatError):
LogHandlerConf._convert_str_to_handlertype("protobufstream(bad-format)")


def test_make_ers_handler_conf_raises_when_env_missing(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr("os.getenv", lambda _: None)
with pytest.raises(ERSEnvError):
LogHandlerConf._make_ers_handler_conf("DUNEDAQ_ERS_ERROR")


def test_make_ers_handler_conf_parses_multiple_handlers(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(
"os.getenv",
lambda _: "erstrace, throttle, lstdout, protobufstream(host:1234)",
)

conf = LogHandlerConf._make_ers_handler_conf("DUNEDAQ_ERS_ERROR")

assert HandlerType.Throttle in conf.handlers
assert HandlerType.Lstdout in conf.handlers
assert conf.protobufconf == ProtobufConf(url="host", port=1234)


def test_get_oks_conf_builds_mapping_for_all_ers_level_vars(
monkeypatch: pytest.MonkeyPatch,
) -> None:
calls: list[str] = []

def _fake_make(level_var: str) -> ERSPyLogHandlerConf:
calls.append(level_var)
return ERSPyLogHandlerConf(handlers=[HandlerType.Rich])

monkeypatch.setattr(
LogHandlerConf, "_make_ers_handler_conf", staticmethod(_fake_make)
)

conf = LogHandlerConf._get_oks_conf()

assert set(calls) == set(level_to_ers_var.values())
assert set(conf.keys()) == set(level_to_ers_var.values())


# demonstrator test_* parity integrated into handlerconf tests

def test_demo_test_handlerconf_runs_ers_flow_and_restores(
monkeypatch: pytest.MonkeyPatch,
) -> None:
logger = MagicMock(spec=logging.Logger)

class FakeHC:
Base: ClassVar[dict] = {
"handlers": {HandlerType.Stream},
"stream": StreamType.BASE,
}
Opmon: ClassVar[dict] = {
"handlers": {HandlerType.Rich},
"stream": StreamType.OPMON,
}

def __init__(self, init_ers: bool = False) -> None:
self._ers = {
"ers_handlers": {
"DUNEDAQ_ERS_ERROR": ERSPyLogHandlerConf(
handlers=[HandlerType.Rich]
),
},
"stream": StreamType.ERS,
}
if init_ers:
self.init_ers_stream()

@property
def ERS(self) -> dict: # noqa: N802
return self._ers

def init_ers_stream(self) -> None:
return None

monkeypatch.setattr(demo, "LogHandlerConf", FakeHC)

demo.test_handlerconf(logger)

logger.warning.assert_called()
logger.info.assert_called()


def test_demo_test_ers_handler_configuration_calls_setup_and_logs(
monkeypatch: pytest.MonkeyPatch,
) -> None:
logger_name = f"demo.ers.{uuid.uuid4()}"
logger = logging.getLogger(logger_name)
logger.handlers = []
logger.filters = []
logger.propagate = False

get_logger_mock = MagicMock(return_value=logger)
setup_mock = MagicMock()

class FakeHC:
def __init__(self, init_ers: bool = False) -> None:
assert init_ers is True
self._ers = {"stream": StreamType.ERS, "ers_handlers": {}}

@property
def ERS(self) -> dict: # noqa: N802
return self._ers

monkeypatch.setattr(demo, "get_daq_logger", get_logger_mock)
monkeypatch.setattr(demo, "setup_daq_ers_logger", setup_mock)
monkeypatch.setattr(demo, "LogHandlerConf", FakeHC)

demo.test_ers_handler_configuration("INFO")

get_logger_mock.assert_called_once()
setup_mock.assert_called_once_with(logger, "session_temp")

logging.root.manager.loggerDict.pop(logger_name, None)
Loading
Loading