1# -*- coding: utf-8 -*-
2
3# This file is dual licensed under the terms of the Apache License, Version
4# 2.0, and the MIT License.  See the LICENSE file in the root of this
5# repository for complete details.
6
7from __future__ import absolute_import, division, print_function
8
9import pytest
10import six
11
12from structlog import dev
13
14
15class TestPad(object):
16    def test_normal(self):
17        """
18        If chars are missing, adequate number of " " are added.
19        """
20        assert 100 == len(dev._pad("test", 100))
21
22    def test_negative(self):
23        """
24        If string is already too long, don't do anything.
25        """
26        assert len("test") == len(dev._pad("test", 2))
27
28
29@pytest.fixture
30def cr():
31    return dev.ConsoleRenderer(colors=dev._has_colorama)
32
33
34@pytest.fixture
35def styles(cr):
36    return cr._styles
37
38
39@pytest.fixture
40def padded(styles):
41    return (
42        styles.bright + dev._pad("test", dev._EVENT_WIDTH) + styles.reset + " "
43    )
44
45
46@pytest.fixture
47def unpadded(styles):
48    return styles.bright + "test" + styles.reset
49
50
51class TestConsoleRenderer(object):
52    @pytest.mark.skipif(dev._has_colorama, reason="Colorama must be missing.")
53    def test_missing_colorama(self):
54        """
55        ConsoleRenderer(colors=True) raises SystemError on initialization if
56        colorama is missing.
57        """
58        with pytest.raises(SystemError) as e:
59            dev.ConsoleRenderer()
60
61        assert (
62            "ConsoleRenderer with `colors=True` requires the colorama package "
63            "installed."
64        ) in e.value.args[0]
65
66    def test_plain(self, cr, styles, unpadded):
67        """
68        Works with a plain event_dict with only the event.
69        """
70        rv = cr(None, None, {"event": "test"})
71
72        assert unpadded == rv
73
74    def test_timestamp(self, cr, styles, unpadded):
75        """
76        Timestamps get prepended.
77        """
78        rv = cr(None, None, {"event": "test", "timestamp": 42})
79
80        assert (styles.timestamp + "42" + styles.reset + " " + unpadded) == rv
81
82    def test_level(self, cr, styles, padded):
83        """
84        Levels are rendered aligned, in square brackets, and color coded.
85        """
86        rv = cr(
87            None, None, {"event": "test", "level": "critical", "foo": "bar"}
88        )
89
90        # fmt: off
91        assert (
92            "[" + dev.RED + styles.bright +
93            dev._pad("critical", cr._longest_level) +
94            styles.reset + "] " +
95            padded +
96            styles.kv_key + "foo" + styles.reset + "=" +
97            styles.kv_value + "bar" + styles.reset
98        ) == rv
99        # fmt: on
100
101    def test_init_accepts_overriding_levels(self, styles, padded):
102        """
103        Stdlib levels are rendered aligned, in brackets, and color coded.
104        """
105        my_styles = dev.ConsoleRenderer.get_default_level_styles(
106            colors=dev._has_colorama
107        )
108        my_styles["MY_OH_MY"] = my_styles["critical"]
109        cr = dev.ConsoleRenderer(
110            colors=dev._has_colorama, level_styles=my_styles
111        )
112
113        # this would blow up if the level_styles override failed
114        rv = cr(
115            None, None, {"event": "test", "level": "MY_OH_MY", "foo": "bar"}
116        )
117
118        # fmt: off
119        assert (
120            "[" + dev.RED + styles.bright +
121            dev._pad("MY_OH_MY", cr._longest_level) +
122            styles.reset + "] " +
123            padded +
124            styles.kv_key + "foo" + styles.reset + "=" +
125            styles.kv_value + "bar" + styles.reset
126        ) == rv
127        # fmt: on
128
129    def test_logger_name(self, cr, styles, padded):
130        """
131        Logger names are appended after the event.
132        """
133        rv = cr(None, None, {"event": "test", "logger": "some_module"})
134
135        # fmt: off
136        assert (
137            padded +
138            "[" + dev.BLUE + styles.bright +
139            "some_module" +
140            styles.reset + "] "
141        ) == rv
142        # fmt: on
143
144    def test_key_values(self, cr, styles, padded):
145        """
146        Key-value pairs go sorted alphabetically to the end.
147        """
148        rv = cr(None, None, {"event": "test", "key": "value", "foo": "bar"})
149
150        # fmt: off
151        assert (
152            padded +
153            styles.kv_key + "foo" + styles.reset + "=" +
154            styles.kv_value + "bar" +
155            styles.reset + " " +
156            styles.kv_key + "key" + styles.reset + "=" +
157            styles.kv_value + "value" +
158            styles.reset
159        ) == rv
160        # fmt: on
161
162    def test_exception(self, cr, padded):
163        """
164        Exceptions are rendered after a new line.
165        """
166        exc = "Traceback:\nFake traceback...\nFakeError: yolo"
167
168        rv = cr(None, None, {"event": "test", "exception": exc})
169
170        assert (padded + "\n" + exc) == rv
171
172    def test_stack_info(self, cr, padded):
173        """
174        Stack traces are rendered after a new line.
175        """
176        stack = "fake stack"
177        rv = cr(None, None, {"event": "test", "stack": stack})
178
179        assert (padded + "\n" + stack) == rv
180
181    def test_pad_event_param(self, styles):
182        """
183        `pad_event` parameter works.
184        """
185        rv = dev.ConsoleRenderer(42, dev._has_colorama)(
186            None, None, {"event": "test", "foo": "bar"}
187        )
188
189        # fmt: off
190        assert (
191            styles.bright +
192            dev._pad("test", 42) +
193            styles.reset + " " +
194            styles.kv_key + "foo" + styles.reset + "=" +
195            styles.kv_value + "bar" + styles.reset
196        ) == rv
197        # fmt: on
198
199    def test_everything(self, cr, styles, padded):
200        """
201        Put all cases together.
202        """
203        exc = "Traceback:\nFake traceback...\nFakeError: yolo"
204        stack = "fake stack trace"
205
206        rv = cr(
207            None,
208            None,
209            {
210                "event": "test",
211                "exception": exc,
212                "key": "value",
213                "foo": "bar",
214                "timestamp": "13:13",
215                "logger": "some_module",
216                "level": "error",
217                "stack": stack,
218            },
219        )
220
221        # fmt: off
222        assert (
223            styles.timestamp + "13:13" + styles.reset +
224            " [" + styles.level_error + styles.bright +
225            dev._pad("error", cr._longest_level) +
226            styles.reset + "] " +
227            padded +
228            "[" + dev.BLUE + styles.bright +
229            "some_module" +
230            styles.reset + "] " +
231            styles.kv_key + "foo" + styles.reset + "=" +
232            styles.kv_value + "bar" +
233            styles.reset + " " +
234            styles.kv_key + "key" + styles.reset + "=" +
235            styles.kv_value + "value" +
236            styles.reset +
237            "\n" + stack + "\n\n" + "=" * 79 + "\n" +
238            "\n" + exc
239        ) == rv
240        # fmt: on
241
242    def test_colorama_colors_false(self):
243        """
244        If colors is False, don't use colors or styles ever.
245        """
246        plain_cr = dev.ConsoleRenderer(colors=False)
247
248        rv = plain_cr(
249            None, None, {"event": "event", "level": "info", "foo": "bar"}
250        )
251
252        assert dev._PlainStyles is plain_cr._styles
253        assert "[info     ] event                          foo=bar" == rv
254
255    def test_colorama_force_colors(self, styles, padded):
256        """
257        If force_colors is True, use colors even if the destination is non-tty.
258        """
259        cr = dev.ConsoleRenderer(
260            colors=dev._has_colorama, force_colors=dev._has_colorama
261        )
262
263        rv = cr(
264            None, None, {"event": "test", "level": "critical", "foo": "bar"}
265        )
266
267        # fmt: off
268        assert (
269            "[" + dev.RED + styles.bright +
270            dev._pad("critical", cr._longest_level) +
271            styles.reset + "] " +
272            padded +
273            styles.kv_key + "foo" + styles.reset + "=" +
274            styles.kv_value + "bar" + styles.reset
275        ) == rv
276        # fmt: on
277
278        assert not dev._has_colorama or dev._ColorfulStyles is cr._styles
279
280    @pytest.mark.parametrize("rns", [True, False])
281    def test_repr_native_str(self, rns):
282        """
283        repr_native_str=False doesn't repr on native strings.  "event" is
284        never repr'ed.
285        """
286        rv = dev.ConsoleRenderer(colors=False, repr_native_str=rns)(
287            None, None, {"event": "哈", "key": 42, "key2": "哈"}
288        )
289
290        cnt = rv.count("哈")
291        if rns and six.PY2:
292            assert 1 == cnt
293        else:
294            assert 2 == cnt
295