Skip to content
Merged
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
72 changes: 72 additions & 0 deletions tests/test_formatters_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Optional fields and value handling in the JSON formatters (issue #137)."""
import json
import logging

import pytest

import json_logging
from json_logging.formatters import BaseJSONFormatter, JSONLogFormatter


@pytest.fixture
def component_settings():
"""Restore the module level component settings, which are read when a formatter is constructed."""
saved = (json_logging.COMPONENT_ID, json_logging.COMPONENT_NAME, json_logging.COMPONENT_INSTANCE_INDEX)
# base_object_common is a class level dict that formatter construction writes into, so restore it as well
saved_common = dict(BaseJSONFormatter.base_object_common)
for key in ("component_id", "component_name", "component_instance_idx"):
BaseJSONFormatter.base_object_common.pop(key, None)
yield
BaseJSONFormatter.base_object_common.clear()
BaseJSONFormatter.base_object_common.update(saved_common)
json_logging.COMPONENT_ID, json_logging.COMPONENT_NAME, json_logging.COMPONENT_INSTANCE_INDEX = saved


def _format(**extra):
record = logging.LogRecord("formatter-test", logging.INFO, __file__, 1, "hello", None, None)
for key, value in extra.items():
setattr(record, key, value)
return json.loads(JSONLogFormatter().format(record))


def test_component_settings_are_added_when_set(component_settings):
json_logging.COMPONENT_ID = "id-1"
json_logging.COMPONENT_NAME = "name-1"
json_logging.COMPONENT_INSTANCE_INDEX = 2

output = _format()

assert output["component_id"] == "id-1"
assert output["component_name"] == "name-1"
assert output["component_instance_idx"] == 2


def test_component_settings_are_omitted_by_default(component_settings):
output = _format()

assert "component_id" not in output
assert "component_name" not in output
assert "component_instance_idx" not in output


def test_non_serialisable_extra_value_falls_back_to_repr():
class Unserialisable:
def __repr__(self):
return "<unserialisable>"

output = _format(thing=Unserialisable())

assert output["thing"] == "<unserialisable>"


def test_props_dict_is_merged_into_the_root_of_the_log():
output = _format(props={"user": "alice", "attempt": 3})

assert output["user"] == "alice"
assert output["attempt"] == 3


def test_props_that_is_not_a_dict_is_not_merged():
output = _format(props="not-a-dict")

assert "not-a-dict" not in output.values()
75 changes: 75 additions & 0 deletions tests/test_init_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Argument validation and error paths of the public initialisation API (issue #137)."""
import logging

import pytest
from helpers.imports import undo_imports_from_package


@pytest.fixture
def fresh_json_logging():
"""Import a pristine json_logging so module level init state does not leak between tests."""
undo_imports_from_package("json_logging")
import json_logging
yield json_logging
undo_imports_from_package("json_logging")


def test_init_framework_twice_raises(fresh_json_logging):
fresh_json_logging.init_flask()
with pytest.raises(RuntimeError, match="more than once"):
fresh_json_logging.init_flask()


def test_init_rejects_custom_formatter_that_is_not_a_formatter(fresh_json_logging):
with pytest.raises(ValueError, match="custom_formatter"):
fresh_json_logging.init_non_web(custom_formatter=dict)


def test_init_rejects_unknown_framework(fresh_json_logging):
with pytest.raises(RuntimeError, match="not a supported framework"):
fresh_json_logging.init_non_web(framework_name="no-such-framework")


def test_init_request_instrument_before_init_raises(fresh_json_logging):
with pytest.raises(RuntimeError, match="init the logging first"):
fresh_json_logging.init_request_instrument(app=object())


def test_init_request_instrument_after_non_web_init_raises(fresh_json_logging):
fresh_json_logging.init_non_web()
with pytest.raises(RuntimeError, match="init the logging first"):
fresh_json_logging.init_request_instrument(app=object())


def test_init_request_instrument_rejects_bad_custom_formatter(fresh_json_logging):
fresh_json_logging.init_flask()
with pytest.raises(ValueError, match="custom_formatter"):
fresh_json_logging.init_request_instrument(app=object(), custom_formatter=dict)


def test_init_request_instrument_rejects_bad_request_response_dto_class(fresh_json_logging):
fresh_json_logging.init_flask()
with pytest.raises(ValueError, match="request_response_dto_class"):
fresh_json_logging.init_request_instrument(app=object(), request_response_dto_class=dict)


def test_get_request_logger_before_init_raises(fresh_json_logging):
with pytest.raises(RuntimeError, match="only available"):
fresh_json_logging.get_request_logger()


def test_config_root_logger_without_handlers_logs_error_and_returns(fresh_json_logging, monkeypatch):
fresh_json_logging.ENABLE_JSON_LOGGING = True
monkeypatch.setattr(logging.root, "handlers", [])
errors = []
monkeypatch.setattr(fresh_json_logging._logger, "error", lambda msg, *a, **kw: errors.append(msg))

def fail(*args, **kwargs):
raise AssertionError("root logger must not be touched when it has no handlers")

monkeypatch.setattr(fresh_json_logging.util, "update_formatter_for_loggers", fail)

fresh_json_logging.config_root_logger()

assert len(errors) == 1
assert "No logging handlers found" in errors[0]
43 changes: 43 additions & 0 deletions tests/test_util_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Error paths of the helpers in json_logging.util (issue #137)."""
import logging

import pytest

from json_logging import util


def test_update_formatter_for_loggers_rejects_non_logger():
with pytest.raises(RuntimeError):
util.update_formatter_for_loggers(["not-a-logger"], logging.Formatter)


def test_update_formatter_for_loggers_replaces_other_formatters_only():
class Wanted(logging.Formatter):
pass

logger = logging.Logger("util-validation-test")
replaced = logging.StreamHandler()
kept = logging.StreamHandler()
already_wanted = Wanted()
kept.setFormatter(already_wanted)
logger.addHandler(replaced)
logger.addHandler(kept)

util.update_formatter_for_loggers([logger], Wanted)

assert isinstance(replaced.formatter, Wanted)
assert kept.formatter is already_wanted


def test_validate_subclass_accepts_subclass():
assert util.validate_subclass(logging.StreamHandler, logging.Handler) is True


def test_validate_subclass_rejects_non_subclass():
with pytest.raises(RuntimeError, match="is not a subclass of"):
util.validate_subclass(dict, logging.Handler)


@pytest.mark.parametrize("value, default, expected", [("7", 1, 7), ("x", 1, 1), (None, 3, 3), (4, 0, 4)])
def test_parse_int_falls_back_to_default(value, default, expected):
assert util.parse_int(value, default) == expected
Loading