1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244
|
# SPDX-License-Identifier: MIT OR Apache-2.0
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the MIT License. See the LICENSE file in the root of this
# repository for complete details.
import pytest
from pretend import raiser, stub
from structlog import get_context
from structlog._base import BoundLoggerBase
from structlog._config import _CONFIG
from structlog.exceptions import DropEvent
from structlog.processors import KeyValueRenderer
from structlog.testing import ReturnLogger
from tests.utils import CustomError
def build_bl(logger=None, processors=None, context=None):
"""
Convenience function to build BoundLoggerBases with sane defaults.
"""
return BoundLoggerBase(
logger if logger is not None else ReturnLogger(),
processors if processors is not None else _CONFIG.default_processors,
context if context is not None else _CONFIG.default_context_class(),
)
class TestBinding:
def test_repr(self):
"""
repr() of a BoundLoggerBase shows its context and processors.
"""
bl = build_bl(processors=[1, 2, 3], context={"A": "B"})
assert (
"<BoundLoggerBase(context={'A': 'B'}, processors=[1, 2, 3])>"
) == repr(bl)
def test_binds_independently(self):
"""
Ensure BoundLogger is immutable by default.
"""
b = build_bl(processors=[KeyValueRenderer(sort_keys=True)])
b = b.bind(x=42, y=23)
b1 = b.bind(foo="bar")
b2 = b.bind(foo="qux")
assert b._context != b1._context != b2._context
def test_new_clears_state(self):
"""
Calling new() on a logger clears the context.
"""
b = build_bl()
b = b.bind(x=42)
assert 42 == get_context(b)["x"]
b = b.bind()
assert 42 == get_context(b)["x"]
b = b.new()
assert {} == dict(get_context(b))
def test_comparison(self):
"""
Two bound loggers are equal if their context is equal.
"""
b = build_bl()
assert b == b.bind()
assert b is not b.bind()
assert b != b.bind(x=5)
assert b != "test"
def test_bind_keeps_class(self):
"""
Binding values does not change the type of the bound logger.
"""
class Wrapper(BoundLoggerBase):
pass
b = Wrapper(None, [], {})
assert isinstance(b.bind(), Wrapper)
def test_new_keeps_class(self):
"""
Clearing context does not change the type of the bound logger.
"""
class Wrapper(BoundLoggerBase):
pass
b = Wrapper(None, [], {})
assert isinstance(b.new(), Wrapper)
def test_unbind(self):
"""
unbind() removes keys from context.
"""
b = build_bl().bind(x=42, y=23).unbind("x", "y")
assert {} == b._context
def test_unbind_fail(self):
"""
unbind() raises KeyError if the key is missing.
"""
with pytest.raises(KeyError):
build_bl().bind(x=42, y=23).unbind("x", "z")
def test_try_unbind(self):
"""
try_unbind() removes keys from context.
"""
b = build_bl().bind(x=42, y=23).try_unbind("x", "y")
assert {} == b._context
def test_try_unbind_fail(self):
"""
try_unbind() does nothing if the key is missing.
"""
b = build_bl().bind(x=42, y=23).try_unbind("x", "z")
assert {"y": 23} == b._context
class TestProcessing:
def test_event_empty_string(self):
"""
Empty strings are a valid event.
"""
b = build_bl(processors=[], context={})
args, kw = b._process_event("meth", "", {"foo": "bar"})
assert () == args
assert {"event": "", "foo": "bar"} == kw
def test_copies_context_before_processing(self):
"""
BoundLoggerBase._process_event() gets called before relaying events
to wrapped loggers.
"""
def chk(_, __, event_dict):
assert b._context is not event_dict
return ""
b = build_bl(processors=[chk])
assert (("",), {}) == b._process_event("", "event", {})
assert "event" not in b._context
def test_chain_does_not_swallow_all_exceptions(self):
"""
If the chain raises anything else than DropEvent, the error is not
swallowed.
"""
b = build_bl(processors=[raiser(CustomError)])
with pytest.raises(CustomError):
b._process_event("", "boom", {})
def test_last_processor_returns_string(self):
"""
If the final processor returns a string, ``(the_string,), {}`` is
returned.
"""
logger = stub(msg=lambda *args, **kw: (args, kw))
b = build_bl(logger, processors=[lambda *_: "foo"])
assert (("foo",), {}) == b._process_event("", "foo", {})
def test_last_processor_returns_bytes(self):
"""
If the final processor returns bytes, ``(the_bytes,), {}`` is
returned.
"""
logger = stub(msg=lambda *args, **kw: (args, kw))
b = build_bl(logger, processors=[lambda *_: b"foo"])
assert ((b"foo",), {}) == b._process_event(None, "name", {})
def test_last_processor_returns_bytearray(self):
"""
If the final processor returns a bytearray, ``(the_array,), {}`` is
returned.
"""
logger = stub(msg=lambda *args, **kw: (args, kw))
b = build_bl(logger, processors=[lambda *_: bytearray(b"foo")])
assert ((bytearray(b"foo"),), {}) == b._process_event(None, "name", {})
def test_last_processor_returns_tuple(self):
"""
If the final processor returns a tuple, it is just passed through.
"""
logger = stub(msg=lambda *args, **kw: (args, kw))
b = build_bl(
logger, processors=[lambda *_: (("foo",), {"key": "value"})]
)
assert (("foo",), {"key": "value"}) == b._process_event("", "foo", {})
def test_last_processor_returns_dict(self):
"""
If the final processor returns a dict, ``(), the_dict`` is returned.
"""
logger = stub(msg=lambda *args, **kw: (args, kw))
b = build_bl(logger, processors=[lambda *_: {"event": "foo"}])
assert ((), {"event": "foo"}) == b._process_event("", "foo", {})
def test_last_processor_returns_unknown_value(self):
"""
If the final processor returns something unexpected, raise ValueError
with a helpful error message.
"""
logger = stub(msg=lambda *args, **kw: (args, kw))
b = build_bl(logger, processors=[lambda *_: object()])
with pytest.raises(ValueError, match="Last processor didn't return"):
b._process_event("", "foo", {})
class TestProxying:
def test_processor_raising_DropEvent_silently_aborts_chain(self, capsys):
"""
If a processor raises DropEvent, the chain is aborted and nothing is
proxied to the logger.
"""
b = build_bl(processors=[raiser(DropEvent), raiser(ValueError)])
b._proxy_to_logger("", None, x=5)
assert ("", "") == capsys.readouterr()
|