# DExTer : Debugging Experience Tester # ~~~~~~ ~ ~~ ~ ~~ # # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. # See https://llvm.org/LICENSE.txt for license information. # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception """Provides formatted/colored console output on both Windows and Linux. Do not use this module directly, but instead use via the appropriate platform- specific module. """ import abc import re import sys import threading import unittest from io import StringIO from dex.utils.Exceptions import Error class _NullLock(object): def __enter__(self): return None def __exit__(self, *params): pass _lock = threading.Lock() _null_lock = _NullLock() class PreserveAutoColors(object): def __init__(self, pretty_output): self.pretty_output = pretty_output self.orig_values = {} self.properties = [ 'auto_reds', 'auto_yellows', 'auto_greens', 'auto_blues' ] def __enter__(self): for p in self.properties: self.orig_values[p] = getattr(self.pretty_output, p)[:] return self def __exit__(self, *args): for p in self.properties: setattr(self.pretty_output, p, self.orig_values[p]) class Stream(object): def __init__(self, py_, os_=None): self.py = py_ self.os = os_ self.orig_color = None self.color_enabled = self.py.isatty() class PrettyOutputBase(object, metaclass=abc.ABCMeta): stdout = Stream(sys.stdout) stderr = Stream(sys.stderr) def __init__(self): self.auto_reds = [] self.auto_yellows = [] self.auto_greens = [] self.auto_blues = [] self._stack = [] def __enter__(self): return self def __exit__(self, *args): pass def _set_valid_stream(self, stream): if stream is None: return self.__class__.stdout return stream def _write(self, text, stream): text = str(text) # Users can embed color control tags in their output # (e.g. hello world would write the word 'hello' in red and # 'world' in yellow). # This function parses these tags using a very simple recursive # descent. colors = { 'r': self.red, 'y': self.yellow, 'g': self.green, 'b': self.blue, 'd': self.default, 'a': self.auto, } # Find all tags (whether open or close) tags = [ t for t in re.finditer('<([{}/])>'.format(''.join(colors)), text) ] if not tags: # No tags. Just write the text to the current stream and return. # 'unmangling' any tags that have been mangled so that they won't # render as colors (for example in error output from this # function). stream = self._set_valid_stream(stream) stream.py.write(text.replace(r'\>', '>')) return open_tags = [i for i in tags if i.group(1) != '/'] close_tags = [i for i in tags if i.group(1) == '/'] if (len(open_tags) != len(close_tags) or any(o.start() >= c.start() for (o, c) in zip(open_tags, close_tags))): raise Error('open/close tag mismatch in "{}"'.format( text.rstrip()).replace('>', r'\>')) open_tag = open_tags.pop(0) # We know that the tags balance correctly, so figure out where the # corresponding close tag is to the current open tag. tag_nesting = 1 close_tag = None for tag in tags[1:]: if tag.group(1) == '/': tag_nesting -= 1 else: tag_nesting += 1 if tag_nesting == 0: close_tag = tag break else: assert False, text # Use the method on the top of the stack for text prior to the open # tag. before = text[:open_tag.start()] if before: self._stack[-1](before, lock=_null_lock, stream=stream) # Use the specified color for the tag itself. color = open_tag.group(1) within = text[open_tag.end():close_tag.start()] if within: colors[color](within, lock=_null_lock, stream=stream) # Use the method on the top of the stack for text after the close tag. after = text[close_tag.end():] if after: self._stack[-1](after, lock=_null_lock, stream=stream) def flush(self, stream): stream = self._set_valid_stream(stream) stream.py.flush() def auto(self, text, stream=None, lock=_lock): text = str(text) stream = self._set_valid_stream(stream) lines = text.splitlines(True) with lock: for line in lines: # This is just being cute for the sake of cuteness, but why # not? line = line.replace('DExTer', 'DExTer') # Apply the appropriate color method if the expression matches # any of # the patterns we have set up. for fn, regexs in ((self.red, self.auto_reds), (self.yellow, self.auto_yellows), (self.green, self.auto_greens), (self.blue, self.auto_blues)): if any(re.search(regex, line) for regex in regexs): fn(line, stream=stream, lock=_null_lock) break else: self.default(line, stream=stream, lock=_null_lock) def _call_color_impl(self, fn, impl, text, *args, **kwargs): try: self._stack.append(fn) return impl(text, *args, **kwargs) finally: fn = self._stack.pop() @abc.abstractmethod def red_impl(self, text, stream=None, **kwargs): pass def red(self, *args, **kwargs): return self._call_color_impl(self.red, self.red_impl, *args, **kwargs) @abc.abstractmethod def yellow_impl(self, text, stream=None, **kwargs): pass def yellow(self, *args, **kwargs): return self._call_color_impl(self.yellow, self.yellow_impl, *args, **kwargs) @abc.abstractmethod def green_impl(self, text, stream=None, **kwargs): pass def green(self, *args, **kwargs): return self._call_color_impl(self.green, self.green_impl, *args, **kwargs) @abc.abstractmethod def blue_impl(self, text, stream=None, **kwargs): pass def blue(self, *args, **kwargs): return self._call_color_impl(self.blue, self.blue_impl, *args, **kwargs) @abc.abstractmethod def default_impl(self, text, stream=None, **kwargs): pass def default(self, *args, **kwargs): return self._call_color_impl(self.default, self.default_impl, *args, **kwargs) def colortest(self): from itertools import combinations, permutations fns = ((self.red, 'rrr'), (self.yellow, 'yyy'), (self.green, 'ggg'), (self.blue, 'bbb'), (self.default, 'ddd')) for l in range(1, len(fns) + 1): for comb in combinations(fns, l): for perm in permutations(comb): for stream in (None, self.__class__.stderr): perm[0][0]('stdout ' if stream is None else 'stderr ', stream) for fn, string in perm: fn(string, stream) self.default('\n', stream) tests = [ (self.auto, 'default1red2default3'), (self.red, 'red1red2red3'), (self.blue, 'blue1red2blue3'), (self.red, 'red1yellow2red3'), (self.auto, 'default1yellow2red3'), (self.auto, 'default1green2red3'), (self.auto, 'default1green2red3green4default5'), (self.auto, 'default1green2default3green4default5'), (self.auto, 'red1green2red3green4red5'), (self.auto, 'red1green2yellow3green4default5'), (self.auto, 'default1red2'), (self.auto, 'red1default2red3green4default5'), (self.blue, 'red1blue2red3green'), (self.blue, 'rryrb'), ] for fn, text in tests: for stream in (None, self.__class__.stderr): stream_name = 'stdout' if stream is None else 'stderr' fn('{} {}\n'.format(stream_name, text), stream) class TestPrettyOutput(unittest.TestCase): class MockPrettyOutput(PrettyOutputBase): def red_impl(self, text, stream=None, **kwargs): self._write('[R]{}[/R]'.format(text), stream) def yellow_impl(self, text, stream=None, **kwargs): self._write('[Y]{}[/Y]'.format(text), stream) def green_impl(self, text, stream=None, **kwargs): self._write('[G]{}[/G]'.format(text), stream) def blue_impl(self, text, stream=None, **kwargs): self._write('[B]{}[/B]'.format(text), stream) def default_impl(self, text, stream=None, **kwargs): self._write('[D]{}[/D]'.format(text), stream) def test_red(self): with TestPrettyOutput.MockPrettyOutput() as o: stream = Stream(StringIO()) o.red('hello', stream) self.assertEqual(stream.py.getvalue(), '[R]hello[/R]') def test_yellow(self): with TestPrettyOutput.MockPrettyOutput() as o: stream = Stream(StringIO()) o.yellow('hello', stream) self.assertEqual(stream.py.getvalue(), '[Y]hello[/Y]') def test_green(self): with TestPrettyOutput.MockPrettyOutput() as o: stream = Stream(StringIO()) o.green('hello', stream) self.assertEqual(stream.py.getvalue(), '[G]hello[/G]') def test_blue(self): with TestPrettyOutput.MockPrettyOutput() as o: stream = Stream(StringIO()) o.blue('hello', stream) self.assertEqual(stream.py.getvalue(), '[B]hello[/B]') def test_default(self): with TestPrettyOutput.MockPrettyOutput() as o: stream = Stream(StringIO()) o.default('hello', stream) self.assertEqual(stream.py.getvalue(), '[D]hello[/D]') def test_auto(self): with TestPrettyOutput.MockPrettyOutput() as o: stream = Stream(StringIO()) o.auto_reds.append('foo') o.auto('bar\n', stream) o.auto('foo\n', stream) o.auto('baz\n', stream) self.assertEqual(stream.py.getvalue(), '[D]bar\n[/D][R]foo\n[/R][D]baz\n[/D]') stream = Stream(StringIO()) o.auto('bar\nfoo\nbaz\n', stream) self.assertEqual(stream.py.getvalue(), '[D]bar\n[/D][R]foo\n[/R][D]baz\n[/D]') stream = Stream(StringIO()) o.auto('barfoobaz\nbardoobaz\n', stream) self.assertEqual(stream.py.getvalue(), '[R]barfoobaz\n[/R][D]bardoobaz\n[/D]') o.auto_greens.append('doo') stream = Stream(StringIO()) o.auto('barfoobaz\nbardoobaz\n', stream) self.assertEqual(stream.py.getvalue(), '[R]barfoobaz\n[/R][G]bardoobaz\n[/G]') def test_PreserveAutoColors(self): with TestPrettyOutput.MockPrettyOutput() as o: o.auto_reds.append('foo') with PreserveAutoColors(o): o.auto_greens.append('bar') stream = Stream(StringIO()) o.auto('foo\nbar\nbaz\n', stream) self.assertEqual(stream.py.getvalue(), '[R]foo\n[/R][G]bar\n[/G][D]baz\n[/D]') stream = Stream(StringIO()) o.auto('foo\nbar\nbaz\n', stream) self.assertEqual(stream.py.getvalue(), '[R]foo\n[/R][D]bar\n[/D][D]baz\n[/D]') stream = Stream(StringIO()) o.yellow('foobarbaz', stream) self.assertEqual( stream.py.getvalue(), '[Y][Y][/Y][R]foo[/R][Y][Y]bar[/Y][D]baz[/D][Y][/Y][/Y][/Y]') def test_tags(self): with TestPrettyOutput.MockPrettyOutput() as o: stream = Stream(StringIO()) o.auto('hi', stream) self.assertEqual(stream.py.getvalue(), '[D][D][/D][R]hi[/R][D][/D][/D]') stream = Stream(StringIO()) o.auto('abc', stream) self.assertEqual( stream.py.getvalue(), '[D][D][/D][R][R][/R][Y]a[/Y][R]b[/R][/R][D]c[/D][/D]') with self.assertRaisesRegex(Error, 'tag mismatch'): o.auto('hi', stream) with self.assertRaisesRegex(Error, 'tag mismatch'): o.auto('hi', stream) with self.assertRaisesRegex(Error, 'tag mismatch'): o.auto('hi', stream) with self.assertRaisesRegex(Error, 'tag mismatch'): o.auto('hi', stream) with self.assertRaisesRegex(Error, 'tag mismatch'): o.auto('hi', stream)