Mercurial > repos > shellac > sam_consensus_v3
comparison env/lib/python3.9/site-packages/coloredlogs/tests.py @ 0:4f3585e2f14b draft default tip
"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author | shellac |
---|---|
date | Mon, 22 Mar 2021 18:12:50 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4f3585e2f14b |
---|---|
1 # Automated tests for the `coloredlogs' package. | |
2 # | |
3 # Author: Peter Odding <peter@peterodding.com> | |
4 # Last Change: December 10, 2020 | |
5 # URL: https://coloredlogs.readthedocs.io | |
6 | |
7 """Automated tests for the `coloredlogs` package.""" | |
8 | |
9 # Standard library modules. | |
10 import contextlib | |
11 import logging | |
12 import logging.handlers | |
13 import os | |
14 import re | |
15 import subprocess | |
16 import sys | |
17 import tempfile | |
18 | |
19 # External dependencies. | |
20 from humanfriendly.compat import StringIO | |
21 from humanfriendly.terminal import ANSI_COLOR_CODES, ANSI_CSI, ansi_style, ansi_wrap | |
22 from humanfriendly.testing import PatchedAttribute, PatchedItem, TestCase, retry | |
23 from humanfriendly.text import format, random_string | |
24 | |
25 # The module we're testing. | |
26 import coloredlogs | |
27 import coloredlogs.cli | |
28 from coloredlogs import ( | |
29 CHROOT_FILES, | |
30 ColoredFormatter, | |
31 NameNormalizer, | |
32 decrease_verbosity, | |
33 find_defined_levels, | |
34 find_handler, | |
35 find_hostname, | |
36 find_program_name, | |
37 find_username, | |
38 get_level, | |
39 increase_verbosity, | |
40 install, | |
41 is_verbose, | |
42 level_to_number, | |
43 match_stream_handler, | |
44 parse_encoded_styles, | |
45 set_level, | |
46 walk_propagation_tree, | |
47 ) | |
48 from coloredlogs.demo import demonstrate_colored_logging | |
49 from coloredlogs.syslog import SystemLogging, is_syslog_supported, match_syslog_handler | |
50 from coloredlogs.converter import ( | |
51 ColoredCronMailer, | |
52 EIGHT_COLOR_PALETTE, | |
53 capture, | |
54 convert, | |
55 ) | |
56 | |
57 # External test dependencies. | |
58 from capturer import CaptureOutput | |
59 from verboselogs import VerboseLogger | |
60 | |
61 # Compiled regular expression that matches a single line of output produced by | |
62 # the default log format (does not include matching of ANSI escape sequences). | |
63 PLAIN_TEXT_PATTERN = re.compile(r''' | |
64 (?P<date> \d{4}-\d{2}-\d{2} ) | |
65 \s (?P<time> \d{2}:\d{2}:\d{2} ) | |
66 \s (?P<hostname> \S+ ) | |
67 \s (?P<logger_name> \w+ ) | |
68 \[ (?P<process_id> \d+ ) \] | |
69 \s (?P<severity> [A-Z]+ ) | |
70 \s (?P<message> .* ) | |
71 ''', re.VERBOSE) | |
72 | |
73 # Compiled regular expression that matches a single line of output produced by | |
74 # the default log format with milliseconds=True. | |
75 PATTERN_INCLUDING_MILLISECONDS = re.compile(r''' | |
76 (?P<date> \d{4}-\d{2}-\d{2} ) | |
77 \s (?P<time> \d{2}:\d{2}:\d{2},\d{3} ) | |
78 \s (?P<hostname> \S+ ) | |
79 \s (?P<logger_name> \w+ ) | |
80 \[ (?P<process_id> \d+ ) \] | |
81 \s (?P<severity> [A-Z]+ ) | |
82 \s (?P<message> .* ) | |
83 ''', re.VERBOSE) | |
84 | |
85 | |
86 def setUpModule(): | |
87 """Speed up the tests by disabling the demo's artificial delay.""" | |
88 os.environ['COLOREDLOGS_DEMO_DELAY'] = '0' | |
89 coloredlogs.demo.DEMO_DELAY = 0 | |
90 | |
91 | |
92 class ColoredLogsTestCase(TestCase): | |
93 | |
94 """Container for the `coloredlogs` tests.""" | |
95 | |
96 def find_system_log(self): | |
97 """Find the system log file or skip the current test.""" | |
98 filename = ('/var/log/system.log' if sys.platform == 'darwin' else ( | |
99 '/var/log/syslog' if 'linux' in sys.platform else None | |
100 )) | |
101 if not filename: | |
102 self.skipTest("Location of system log file unknown!") | |
103 elif not os.path.isfile(filename): | |
104 self.skipTest("System log file not found! (%s)" % filename) | |
105 elif not os.access(filename, os.R_OK): | |
106 self.skipTest("Insufficient permissions to read system log file! (%s)" % filename) | |
107 else: | |
108 return filename | |
109 | |
110 def test_level_to_number(self): | |
111 """Make sure :func:`level_to_number()` works as intended.""" | |
112 # Make sure the default levels are translated as expected. | |
113 assert level_to_number('debug') == logging.DEBUG | |
114 assert level_to_number('info') == logging.INFO | |
115 assert level_to_number('warning') == logging.WARNING | |
116 assert level_to_number('error') == logging.ERROR | |
117 assert level_to_number('fatal') == logging.FATAL | |
118 # Make sure bogus level names don't blow up. | |
119 assert level_to_number('bogus-level') == logging.INFO | |
120 | |
121 def test_find_hostname(self): | |
122 """Make sure :func:`~find_hostname()` works correctly.""" | |
123 assert find_hostname() | |
124 # Create a temporary file as a placeholder for e.g. /etc/debian_chroot. | |
125 fd, temporary_file = tempfile.mkstemp() | |
126 try: | |
127 with open(temporary_file, 'w') as handle: | |
128 handle.write('first line\n') | |
129 handle.write('second line\n') | |
130 CHROOT_FILES.insert(0, temporary_file) | |
131 # Make sure the chroot file is being read. | |
132 assert find_hostname() == 'first line' | |
133 finally: | |
134 # Clean up. | |
135 CHROOT_FILES.pop(0) | |
136 os.unlink(temporary_file) | |
137 # Test that unreadable chroot files don't break coloredlogs. | |
138 try: | |
139 CHROOT_FILES.insert(0, temporary_file) | |
140 # Make sure that a usable value is still produced. | |
141 assert find_hostname() | |
142 finally: | |
143 # Clean up. | |
144 CHROOT_FILES.pop(0) | |
145 | |
146 def test_host_name_filter(self): | |
147 """Make sure :func:`install()` integrates with :class:`~coloredlogs.HostNameFilter()`.""" | |
148 install(fmt='%(hostname)s') | |
149 with CaptureOutput() as capturer: | |
150 logging.info("A truly insignificant message ..") | |
151 output = capturer.get_text() | |
152 assert find_hostname() in output | |
153 | |
154 def test_program_name_filter(self): | |
155 """Make sure :func:`install()` integrates with :class:`~coloredlogs.ProgramNameFilter()`.""" | |
156 install(fmt='%(programname)s') | |
157 with CaptureOutput() as capturer: | |
158 logging.info("A truly insignificant message ..") | |
159 output = capturer.get_text() | |
160 assert find_program_name() in output | |
161 | |
162 def test_username_filter(self): | |
163 """Make sure :func:`install()` integrates with :class:`~coloredlogs.UserNameFilter()`.""" | |
164 install(fmt='%(username)s') | |
165 with CaptureOutput() as capturer: | |
166 logging.info("A truly insignificant message ..") | |
167 output = capturer.get_text() | |
168 assert find_username() in output | |
169 | |
170 def test_system_logging(self): | |
171 """Make sure the :class:`coloredlogs.syslog.SystemLogging` context manager works.""" | |
172 system_log_file = self.find_system_log() | |
173 expected_message = random_string(50) | |
174 with SystemLogging(programname='coloredlogs-test-suite') as syslog: | |
175 if not syslog: | |
176 return self.skipTest("couldn't connect to syslog daemon") | |
177 # When I tried out the system logging support on macOS 10.13.1 on | |
178 # 2018-01-05 I found that while WARNING and ERROR messages show up | |
179 # in the system log DEBUG and INFO messages don't. This explains | |
180 # the importance of the level of the log message below. | |
181 logging.error("%s", expected_message) | |
182 # Retry the following assertion (for up to 60 seconds) to give the | |
183 # logging daemon time to write our log message to disk. This | |
184 # appears to be needed on MacOS workers on Travis CI, see: | |
185 # https://travis-ci.org/xolox/python-coloredlogs/jobs/325245853 | |
186 retry(lambda: check_contents(system_log_file, expected_message, True)) | |
187 | |
188 def test_system_logging_override(self): | |
189 """Make sure the :class:`coloredlogs.syslog.is_syslog_supported` respects the override.""" | |
190 with PatchedItem(os.environ, 'COLOREDLOGS_SYSLOG', 'true'): | |
191 assert is_syslog_supported() is True | |
192 with PatchedItem(os.environ, 'COLOREDLOGS_SYSLOG', 'false'): | |
193 assert is_syslog_supported() is False | |
194 | |
195 def test_syslog_shortcut_simple(self): | |
196 """Make sure that ``coloredlogs.install(syslog=True)`` works.""" | |
197 system_log_file = self.find_system_log() | |
198 expected_message = random_string(50) | |
199 with cleanup_handlers(): | |
200 # See test_system_logging() for the importance of this log level. | |
201 coloredlogs.install(syslog=True) | |
202 logging.error("%s", expected_message) | |
203 # See the comments in test_system_logging() on why this is retried. | |
204 retry(lambda: check_contents(system_log_file, expected_message, True)) | |
205 | |
206 def test_syslog_shortcut_enhanced(self): | |
207 """Make sure that ``coloredlogs.install(syslog='warning')`` works.""" | |
208 system_log_file = self.find_system_log() | |
209 the_expected_message = random_string(50) | |
210 not_an_expected_message = random_string(50) | |
211 with cleanup_handlers(): | |
212 # See test_system_logging() for the importance of these log levels. | |
213 coloredlogs.install(syslog='error') | |
214 logging.warning("%s", not_an_expected_message) | |
215 logging.error("%s", the_expected_message) | |
216 # See the comments in test_system_logging() on why this is retried. | |
217 retry(lambda: check_contents(system_log_file, the_expected_message, True)) | |
218 retry(lambda: check_contents(system_log_file, not_an_expected_message, False)) | |
219 | |
220 def test_name_normalization(self): | |
221 """Make sure :class:`~coloredlogs.NameNormalizer` works as intended.""" | |
222 nn = NameNormalizer() | |
223 for canonical_name in ['debug', 'info', 'warning', 'error', 'critical']: | |
224 assert nn.normalize_name(canonical_name) == canonical_name | |
225 assert nn.normalize_name(canonical_name.upper()) == canonical_name | |
226 assert nn.normalize_name('warn') == 'warning' | |
227 assert nn.normalize_name('fatal') == 'critical' | |
228 | |
229 def test_style_parsing(self): | |
230 """Make sure :func:`~coloredlogs.parse_encoded_styles()` works as intended.""" | |
231 encoded_styles = 'debug=green;warning=yellow;error=red;critical=red,bold' | |
232 decoded_styles = parse_encoded_styles(encoded_styles, normalize_key=lambda k: k.upper()) | |
233 assert sorted(decoded_styles.keys()) == sorted(['debug', 'warning', 'error', 'critical']) | |
234 assert decoded_styles['debug']['color'] == 'green' | |
235 assert decoded_styles['warning']['color'] == 'yellow' | |
236 assert decoded_styles['error']['color'] == 'red' | |
237 assert decoded_styles['critical']['color'] == 'red' | |
238 assert decoded_styles['critical']['bold'] is True | |
239 | |
240 def test_is_verbose(self): | |
241 """Make sure is_verbose() does what it should :-).""" | |
242 set_level(logging.INFO) | |
243 assert not is_verbose() | |
244 set_level(logging.DEBUG) | |
245 assert is_verbose() | |
246 set_level(logging.VERBOSE) | |
247 assert is_verbose() | |
248 | |
249 def test_increase_verbosity(self): | |
250 """Make sure increase_verbosity() respects default and custom levels.""" | |
251 # Start from a known state. | |
252 set_level(logging.INFO) | |
253 assert get_level() == logging.INFO | |
254 # INFO -> VERBOSE. | |
255 increase_verbosity() | |
256 assert get_level() == logging.VERBOSE | |
257 # VERBOSE -> DEBUG. | |
258 increase_verbosity() | |
259 assert get_level() == logging.DEBUG | |
260 # DEBUG -> SPAM. | |
261 increase_verbosity() | |
262 assert get_level() == logging.SPAM | |
263 # SPAM -> NOTSET. | |
264 increase_verbosity() | |
265 assert get_level() == logging.NOTSET | |
266 # NOTSET -> NOTSET. | |
267 increase_verbosity() | |
268 assert get_level() == logging.NOTSET | |
269 | |
270 def test_decrease_verbosity(self): | |
271 """Make sure decrease_verbosity() respects default and custom levels.""" | |
272 # Start from a known state. | |
273 set_level(logging.INFO) | |
274 assert get_level() == logging.INFO | |
275 # INFO -> NOTICE. | |
276 decrease_verbosity() | |
277 assert get_level() == logging.NOTICE | |
278 # NOTICE -> WARNING. | |
279 decrease_verbosity() | |
280 assert get_level() == logging.WARNING | |
281 # WARNING -> SUCCESS. | |
282 decrease_verbosity() | |
283 assert get_level() == logging.SUCCESS | |
284 # SUCCESS -> ERROR. | |
285 decrease_verbosity() | |
286 assert get_level() == logging.ERROR | |
287 # ERROR -> CRITICAL. | |
288 decrease_verbosity() | |
289 assert get_level() == logging.CRITICAL | |
290 # CRITICAL -> CRITICAL. | |
291 decrease_verbosity() | |
292 assert get_level() == logging.CRITICAL | |
293 | |
294 def test_level_discovery(self): | |
295 """Make sure find_defined_levels() always reports the levels defined in Python's standard library.""" | |
296 defined_levels = find_defined_levels() | |
297 level_values = defined_levels.values() | |
298 for number in (0, 10, 20, 30, 40, 50): | |
299 assert number in level_values | |
300 | |
301 def test_walk_propagation_tree(self): | |
302 """Make sure walk_propagation_tree() properly walks the tree of loggers.""" | |
303 root, parent, child, grand_child = self.get_logger_tree() | |
304 # Check the default mode of operation. | |
305 loggers = list(walk_propagation_tree(grand_child)) | |
306 assert loggers == [grand_child, child, parent, root] | |
307 # Now change the propagation (non-default mode of operation). | |
308 child.propagate = False | |
309 loggers = list(walk_propagation_tree(grand_child)) | |
310 assert loggers == [grand_child, child] | |
311 | |
312 def test_find_handler(self): | |
313 """Make sure find_handler() works as intended.""" | |
314 root, parent, child, grand_child = self.get_logger_tree() | |
315 # Add some handlers to the tree. | |
316 stream_handler = logging.StreamHandler() | |
317 syslog_handler = logging.handlers.SysLogHandler() | |
318 child.addHandler(stream_handler) | |
319 parent.addHandler(syslog_handler) | |
320 # Make sure the first matching handler is returned. | |
321 matched_handler, matched_logger = find_handler(grand_child, lambda h: isinstance(h, logging.Handler)) | |
322 assert matched_handler is stream_handler | |
323 # Make sure the first matching handler of the given type is returned. | |
324 matched_handler, matched_logger = find_handler(child, lambda h: isinstance(h, logging.handlers.SysLogHandler)) | |
325 assert matched_handler is syslog_handler | |
326 | |
327 def get_logger_tree(self): | |
328 """Create and return a tree of loggers.""" | |
329 # Get the root logger. | |
330 root = logging.getLogger() | |
331 # Create a top level logger for ourselves. | |
332 parent_name = random_string() | |
333 parent = logging.getLogger(parent_name) | |
334 # Create a child logger. | |
335 child_name = '%s.%s' % (parent_name, random_string()) | |
336 child = logging.getLogger(child_name) | |
337 # Create a grand child logger. | |
338 grand_child_name = '%s.%s' % (child_name, random_string()) | |
339 grand_child = logging.getLogger(grand_child_name) | |
340 return root, parent, child, grand_child | |
341 | |
342 def test_support_for_milliseconds(self): | |
343 """Make sure milliseconds are hidden by default but can be easily enabled.""" | |
344 # Check that the default log format doesn't include milliseconds. | |
345 stream = StringIO() | |
346 install(reconfigure=True, stream=stream) | |
347 logging.info("This should not include milliseconds.") | |
348 assert all(map(PLAIN_TEXT_PATTERN.match, stream.getvalue().splitlines())) | |
349 # Check that milliseconds can be enabled via a shortcut. | |
350 stream = StringIO() | |
351 install(milliseconds=True, reconfigure=True, stream=stream) | |
352 logging.info("This should include milliseconds.") | |
353 assert all(map(PATTERN_INCLUDING_MILLISECONDS.match, stream.getvalue().splitlines())) | |
354 | |
355 def test_support_for_milliseconds_directive(self): | |
356 """Make sure milliseconds using the ``%f`` directive are supported.""" | |
357 stream = StringIO() | |
358 install(reconfigure=True, stream=stream, datefmt='%Y-%m-%dT%H:%M:%S.%f%z') | |
359 logging.info("This should be timestamped according to #45.") | |
360 assert re.match(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{4}\s', stream.getvalue()) | |
361 | |
362 def test_plain_text_output_format(self): | |
363 """Inspect the plain text output of coloredlogs.""" | |
364 logger = VerboseLogger(random_string(25)) | |
365 stream = StringIO() | |
366 install(level=logging.NOTSET, logger=logger, stream=stream) | |
367 # Test that filtering on severity works. | |
368 logger.setLevel(logging.INFO) | |
369 logger.debug("No one should see this message.") | |
370 assert len(stream.getvalue().strip()) == 0 | |
371 # Test that the default output format looks okay in plain text. | |
372 logger.setLevel(logging.NOTSET) | |
373 for method, severity in ((logger.debug, 'DEBUG'), | |
374 (logger.info, 'INFO'), | |
375 (logger.verbose, 'VERBOSE'), | |
376 (logger.warning, 'WARNING'), | |
377 (logger.error, 'ERROR'), | |
378 (logger.critical, 'CRITICAL')): | |
379 # XXX Workaround for a regression in Python 3.7 caused by the | |
380 # Logger.isEnabledFor() method using stale cache entries. If we | |
381 # don't clear the cache then logger.isEnabledFor(logging.DEBUG) | |
382 # returns False and no DEBUG message is emitted. | |
383 try: | |
384 logger._cache.clear() | |
385 except AttributeError: | |
386 pass | |
387 # Prepare the text. | |
388 text = "This is a message with severity %r." % severity.lower() | |
389 # Log the message with the given severity. | |
390 method(text) | |
391 # Get the line of output generated by the handler. | |
392 output = stream.getvalue() | |
393 lines = output.splitlines() | |
394 last_line = lines[-1] | |
395 assert text in last_line | |
396 assert severity in last_line | |
397 assert PLAIN_TEXT_PATTERN.match(last_line) | |
398 | |
399 def test_force_enable(self): | |
400 """Make sure ANSI escape sequences can be forced (bypassing auto-detection).""" | |
401 interpreter = subprocess.Popen([ | |
402 sys.executable, "-c", ";".join([ | |
403 "import coloredlogs, logging", | |
404 "coloredlogs.install(isatty=True)", | |
405 "logging.info('Hello world')", | |
406 ]), | |
407 ], stderr=subprocess.PIPE) | |
408 stdout, stderr = interpreter.communicate() | |
409 assert ANSI_CSI in stderr.decode('UTF-8') | |
410 | |
411 def test_auto_disable(self): | |
412 """ | |
413 Make sure ANSI escape sequences are not emitted when logging output is being redirected. | |
414 | |
415 This is a regression test for https://github.com/xolox/python-coloredlogs/issues/100. | |
416 | |
417 It works as follows: | |
418 | |
419 1. We mock an interactive terminal using 'capturer' to ensure that this | |
420 test works inside test drivers that capture output (like pytest). | |
421 | |
422 2. We launch a subprocess (to ensure a clean process state) where | |
423 stderr is captured but stdout is not, emulating issue #100. | |
424 | |
425 3. The output captured on stderr contained ANSI escape sequences after | |
426 this test was written and before the issue was fixed, so now this | |
427 serves as a regression test for issue #100. | |
428 """ | |
429 with CaptureOutput(): | |
430 interpreter = subprocess.Popen([ | |
431 sys.executable, "-c", ";".join([ | |
432 "import coloredlogs, logging", | |
433 "coloredlogs.install()", | |
434 "logging.info('Hello world')", | |
435 ]), | |
436 ], stderr=subprocess.PIPE) | |
437 stdout, stderr = interpreter.communicate() | |
438 assert ANSI_CSI not in stderr.decode('UTF-8') | |
439 | |
440 def test_env_disable(self): | |
441 """Make sure ANSI escape sequences can be disabled using ``$NO_COLOR``.""" | |
442 with PatchedItem(os.environ, 'NO_COLOR', 'I like monochrome'): | |
443 with CaptureOutput() as capturer: | |
444 subprocess.check_call([ | |
445 sys.executable, "-c", ";".join([ | |
446 "import coloredlogs, logging", | |
447 "coloredlogs.install()", | |
448 "logging.info('Hello world')", | |
449 ]), | |
450 ]) | |
451 output = capturer.get_text() | |
452 assert ANSI_CSI not in output | |
453 | |
454 def test_html_conversion(self): | |
455 """Check the conversion from ANSI escape sequences to HTML.""" | |
456 # Check conversion of colored text. | |
457 for color_name, ansi_code in ANSI_COLOR_CODES.items(): | |
458 ansi_encoded_text = 'plain text followed by %s text' % ansi_wrap(color_name, color=color_name) | |
459 expected_html = format( | |
460 '<code>plain text followed by <span style="color:{css}">{name}</span> text</code>', | |
461 css=EIGHT_COLOR_PALETTE[ansi_code], name=color_name, | |
462 ) | |
463 self.assertEqual(expected_html, convert(ansi_encoded_text)) | |
464 # Check conversion of bright colored text. | |
465 expected_html = '<code><span style="color:#FF0">bright yellow</span></code>' | |
466 self.assertEqual(expected_html, convert(ansi_wrap('bright yellow', color='yellow', bright=True))) | |
467 # Check conversion of text with a background color. | |
468 expected_html = '<code><span style="background-color:#DE382B">red background</span></code>' | |
469 self.assertEqual(expected_html, convert(ansi_wrap('red background', background='red'))) | |
470 # Check conversion of text with a bright background color. | |
471 expected_html = '<code><span style="background-color:#F00">bright red background</span></code>' | |
472 self.assertEqual(expected_html, convert(ansi_wrap('bright red background', background='red', bright=True))) | |
473 # Check conversion of text that uses the 256 color mode palette as a foreground color. | |
474 expected_html = '<code><span style="color:#FFAF00">256 color mode foreground</span></code>' | |
475 self.assertEqual(expected_html, convert(ansi_wrap('256 color mode foreground', color=214))) | |
476 # Check conversion of text that uses the 256 color mode palette as a background color. | |
477 expected_html = '<code><span style="background-color:#AF0000">256 color mode background</span></code>' | |
478 self.assertEqual(expected_html, convert(ansi_wrap('256 color mode background', background=124))) | |
479 # Check that invalid 256 color mode indexes don't raise exceptions. | |
480 expected_html = '<code>plain text expected</code>' | |
481 self.assertEqual(expected_html, convert('\x1b[38;5;256mplain text expected\x1b[0m')) | |
482 # Check conversion of bold text. | |
483 expected_html = '<code><span style="font-weight:bold">bold text</span></code>' | |
484 self.assertEqual(expected_html, convert(ansi_wrap('bold text', bold=True))) | |
485 # Check conversion of underlined text. | |
486 expected_html = '<code><span style="text-decoration:underline">underlined text</span></code>' | |
487 self.assertEqual(expected_html, convert(ansi_wrap('underlined text', underline=True))) | |
488 # Check conversion of strike-through text. | |
489 expected_html = '<code><span style="text-decoration:line-through">strike-through text</span></code>' | |
490 self.assertEqual(expected_html, convert(ansi_wrap('strike-through text', strike_through=True))) | |
491 # Check conversion of inverse text. | |
492 expected_html = '<code><span style="background-color:#FFC706;color:#000">inverse</span></code>' | |
493 self.assertEqual(expected_html, convert(ansi_wrap('inverse', color='yellow', inverse=True))) | |
494 # Check conversion of URLs. | |
495 for sample_text in 'www.python.org', 'http://coloredlogs.rtfd.org', 'https://coloredlogs.rtfd.org': | |
496 sample_url = sample_text if '://' in sample_text else ('http://' + sample_text) | |
497 expected_html = '<code><a href="%s" style="color:inherit">%s</a></code>' % (sample_url, sample_text) | |
498 self.assertEqual(expected_html, convert(sample_text)) | |
499 # Check that the capture pattern for URLs doesn't match ANSI escape | |
500 # sequences and also check that the short hand for the 0 reset code is | |
501 # supported. These are tests for regressions of bugs found in | |
502 # coloredlogs <= 8.0. | |
503 reset_short_hand = '\x1b[0m' | |
504 blue_underlined = ansi_style(color='blue', underline=True) | |
505 ansi_encoded_text = '<%shttps://coloredlogs.readthedocs.io%s>' % (blue_underlined, reset_short_hand) | |
506 expected_html = ( | |
507 '<code><<span style="color:#006FB8;text-decoration:underline">' | |
508 '<a href="https://coloredlogs.readthedocs.io" style="color:inherit">' | |
509 'https://coloredlogs.readthedocs.io' | |
510 '</a></span>></code>' | |
511 ) | |
512 self.assertEqual(expected_html, convert(ansi_encoded_text)) | |
513 | |
514 def test_output_interception(self): | |
515 """Test capturing of output from external commands.""" | |
516 expected_output = 'testing, 1, 2, 3 ..' | |
517 actual_output = capture(['echo', expected_output]) | |
518 assert actual_output.strip() == expected_output.strip() | |
519 | |
520 def test_enable_colored_cron_mailer(self): | |
521 """Test that automatic ANSI to HTML conversion when running under ``cron`` can be enabled.""" | |
522 with PatchedItem(os.environ, 'CONTENT_TYPE', 'text/html'): | |
523 with ColoredCronMailer() as mailer: | |
524 assert mailer.is_enabled | |
525 | |
526 def test_disable_colored_cron_mailer(self): | |
527 """Test that automatic ANSI to HTML conversion when running under ``cron`` can be disabled.""" | |
528 with PatchedItem(os.environ, 'CONTENT_TYPE', 'text/plain'): | |
529 with ColoredCronMailer() as mailer: | |
530 assert not mailer.is_enabled | |
531 | |
532 def test_auto_install(self): | |
533 """Test :func:`coloredlogs.auto_install()`.""" | |
534 needle = random_string() | |
535 command_line = [sys.executable, '-c', 'import logging; logging.info(%r)' % needle] | |
536 # Sanity check that log messages aren't enabled by default. | |
537 with CaptureOutput() as capturer: | |
538 os.environ['COLOREDLOGS_AUTO_INSTALL'] = 'false' | |
539 subprocess.check_call(command_line) | |
540 output = capturer.get_text() | |
541 assert needle not in output | |
542 # Test that the $COLOREDLOGS_AUTO_INSTALL environment variable can be | |
543 # used to automatically call coloredlogs.install() during initialization. | |
544 with CaptureOutput() as capturer: | |
545 os.environ['COLOREDLOGS_AUTO_INSTALL'] = 'true' | |
546 subprocess.check_call(command_line) | |
547 output = capturer.get_text() | |
548 assert needle in output | |
549 | |
550 def test_cli_demo(self): | |
551 """Test the command line colored logging demonstration.""" | |
552 with CaptureOutput() as capturer: | |
553 main('coloredlogs', '--demo') | |
554 output = capturer.get_text() | |
555 # Make sure the output contains all of the expected logging level names. | |
556 for name in 'debug', 'info', 'warning', 'error', 'critical': | |
557 assert name.upper() in output | |
558 | |
559 def test_cli_conversion(self): | |
560 """Test the command line HTML conversion.""" | |
561 output = main('coloredlogs', '--convert', 'coloredlogs', '--demo', capture=True) | |
562 # Make sure the output is encoded as HTML. | |
563 assert '<span' in output | |
564 | |
565 def test_empty_conversion(self): | |
566 """ | |
567 Test that conversion of empty output produces no HTML. | |
568 | |
569 This test was added because I found that ``coloredlogs --convert`` when | |
570 used in a cron job could cause cron to send out what appeared to be | |
571 empty emails. On more careful inspection the body of those emails was | |
572 ``<code></code>``. By not emitting the wrapper element when no other | |
573 HTML is generated, cron will not send out an email. | |
574 """ | |
575 output = main('coloredlogs', '--convert', 'true', capture=True) | |
576 assert not output.strip() | |
577 | |
578 def test_implicit_usage_message(self): | |
579 """Test that the usage message is shown when no actions are given.""" | |
580 assert 'Usage:' in main('coloredlogs', capture=True) | |
581 | |
582 def test_explicit_usage_message(self): | |
583 """Test that the usage message is shown when ``--help`` is given.""" | |
584 assert 'Usage:' in main('coloredlogs', '--help', capture=True) | |
585 | |
586 def test_custom_record_factory(self): | |
587 """ | |
588 Test that custom LogRecord factories are supported. | |
589 | |
590 This test is a bit convoluted because the logging module suppresses | |
591 exceptions. We monkey patch the method suspected of encountering | |
592 exceptions so that we can tell after it was called whether any | |
593 exceptions occurred (despite the exceptions not propagating). | |
594 """ | |
595 if not hasattr(logging, 'getLogRecordFactory'): | |
596 return self.skipTest("this test requires Python >= 3.2") | |
597 | |
598 exceptions = [] | |
599 original_method = ColoredFormatter.format | |
600 original_factory = logging.getLogRecordFactory() | |
601 | |
602 def custom_factory(*args, **kwargs): | |
603 record = original_factory(*args, **kwargs) | |
604 record.custom_attribute = 0xdecafbad | |
605 return record | |
606 | |
607 def custom_method(*args, **kw): | |
608 try: | |
609 return original_method(*args, **kw) | |
610 except Exception as e: | |
611 exceptions.append(e) | |
612 raise | |
613 | |
614 with PatchedAttribute(ColoredFormatter, 'format', custom_method): | |
615 logging.setLogRecordFactory(custom_factory) | |
616 try: | |
617 demonstrate_colored_logging() | |
618 finally: | |
619 logging.setLogRecordFactory(original_factory) | |
620 | |
621 # Ensure that no exceptions were triggered. | |
622 assert not exceptions | |
623 | |
624 | |
625 def check_contents(filename, contents, match): | |
626 """Check if a line in a file contains an expected string.""" | |
627 with open(filename) as handle: | |
628 assert any(contents in line for line in handle) == match | |
629 | |
630 | |
631 def main(*arguments, **options): | |
632 """Wrap the command line interface to make it easier to test.""" | |
633 capture = options.get('capture', False) | |
634 saved_argv = sys.argv | |
635 saved_stdout = sys.stdout | |
636 try: | |
637 sys.argv = arguments | |
638 if capture: | |
639 sys.stdout = StringIO() | |
640 coloredlogs.cli.main() | |
641 if capture: | |
642 return sys.stdout.getvalue() | |
643 finally: | |
644 sys.argv = saved_argv | |
645 sys.stdout = saved_stdout | |
646 | |
647 | |
648 @contextlib.contextmanager | |
649 def cleanup_handlers(): | |
650 """Context manager to cleanup output handlers.""" | |
651 # There's nothing to set up so we immediately yield control. | |
652 yield | |
653 # After the with block ends we cleanup any output handlers. | |
654 for match_func in match_stream_handler, match_syslog_handler: | |
655 handler, logger = find_handler(logging.getLogger(), match_func) | |
656 if handler and logger: | |
657 logger.removeHandler(handler) |