You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
393 lines
14 KiB
393 lines
14 KiB
# 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. <r>hello</> <y>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', '<r>D<y>E<g>x<b>T</></>e</>r</>')
|
|
|
|
# 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, 'default1<r>red2</>default3'),
|
|
(self.red, 'red1<r>red2</>red3'),
|
|
(self.blue, 'blue1<r>red2</>blue3'),
|
|
(self.red, 'red1<y>yellow2</>red3'),
|
|
(self.auto, 'default1<y>yellow2<r>red3</></>'),
|
|
(self.auto, 'default1<g>green2<r>red3</></>'),
|
|
(self.auto, 'default1<g>green2<r>red3</>green4</>default5'),
|
|
(self.auto, 'default1<g>green2</>default3<g>green4</>default5'),
|
|
(self.auto, '<r>red1<g>green2</>red3<g>green4</>red5</>'),
|
|
(self.auto, '<r>red1<y><g>green2</>yellow3</>green4</>default5'),
|
|
(self.auto, '<r><y><g><b><d>default1</></><r></></></>red2</>'),
|
|
(self.auto, '<r>red1</>default2<r>red3</><g>green4</>default5'),
|
|
(self.blue, '<r>red1</>blue2<r><r>red3</><g><g>green</></></>'),
|
|
(self.blue, '<r>r<r>r<y>y<r><r><r><r>r</></></></></></></>b'),
|
|
]
|
|
|
|
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('<a>foo</>bar<a>baz</>', 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('<r>hi</>', stream)
|
|
self.assertEqual(stream.py.getvalue(),
|
|
'[D][D][/D][R]hi[/R][D][/D][/D]')
|
|
|
|
stream = Stream(StringIO())
|
|
o.auto('<r><y>a</>b</>c', 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('<r>hi', stream)
|
|
|
|
with self.assertRaisesRegex(Error, 'tag mismatch'):
|
|
o.auto('hi</>', stream)
|
|
|
|
with self.assertRaisesRegex(Error, 'tag mismatch'):
|
|
o.auto('<r><y>hi</>', stream)
|
|
|
|
with self.assertRaisesRegex(Error, 'tag mismatch'):
|
|
o.auto('<r><y>hi</><r></>', stream)
|
|
|
|
with self.assertRaisesRegex(Error, 'tag mismatch'):
|
|
o.auto('</>hi<r>', stream)
|