From 827532796959e0140177c487565b2e73e8004167 Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Sat, 19 Jan 2019 14:57:18 +0100 Subject: [PATCH 1/3] Refactor PrettyPrinter - `PrettyPrinter` is now a proxy to the ReFrame's logger and takes particular care in formatting specially the progress output. It also prefixes errors and warnings with the program name. - The general framework logger is now also capable of colorizing its output. This allows errors and warnings to be colorized when logged from anywhere in the framework. --- reframe/core/logging.py | 15 +++++ reframe/frontend/argparse.py | 8 +++ reframe/frontend/cli.py | 12 ++-- reframe/frontend/executors/__init__.py | 2 +- reframe/frontend/printer.py | 81 +++++--------------------- reframe/utility/__init__.py | 45 ++++++++++++++ 6 files changed, 92 insertions(+), 71 deletions(-) diff --git a/reframe/core/logging.py b/reframe/core/logging.py index dfd9c9867e..a7ef8739ff 100644 --- a/reframe/core/logging.py +++ b/reframe/core/logging.py @@ -14,6 +14,7 @@ import reframe.core.debug as debug import reframe.utility.os_ext as os_ext from reframe.core.exceptions import ConfigError, LoggingError +from reframe.utility import AnsiColorizer # Global configuration options for logging @@ -352,6 +353,7 @@ def __init__(self, logger=None, check=None): } ) self.check = check + self.colorize = False def __repr__(self): return debug.repr(self) @@ -415,6 +417,19 @@ def log(self, level, msg, *args, **kwargs): def verbose(self, message, *args, **kwargs): self.log(VERBOSE, message, *args, **kwargs) + def warning(self, message, *args, **kwargs): + if self.colorize: + message = AnsiColorizer.colorize(message, AnsiColorizer.yellow) + + super().warning(message, *args, **kwargs) + + def error(self, message, *args, **kwargs): + if self.colorize: + message = AnsiColorizer.colorize(message, AnsiColorizer.red) + + super().error(message, *args, **kwargs) + + # A logger that doesn't log anything null_logger = LoggerAdapter() diff --git a/reframe/frontend/argparse.py b/reframe/frontend/argparse.py index dad2bf2fe0..3d5f2bc843 100644 --- a/reframe/frontend/argparse.py +++ b/reframe/frontend/argparse.py @@ -138,3 +138,11 @@ def parse_args(self, args=None, namespace=None): ) return options + + +def format_options(namespace): + """Format parsed arguments in ``namespace``.""" + ret = 'Command-line configuration:\n' + ret += '\n'.join([' %s=%s' % (attr, val) + for attr, val in sorted(namespace.__dict__.items())]) + return ret diff --git a/reframe/frontend/cli.py b/reframe/frontend/cli.py index 97a44c2037..7c3047a9eb 100644 --- a/reframe/frontend/cli.py +++ b/reframe/frontend/cli.py @@ -8,19 +8,20 @@ import reframe.core.config as config import reframe.core.logging as logging import reframe.core.runtime as runtime +import reframe.frontend.argparse as argparse import reframe.frontend.check_filters as filters import reframe.utility as util import reframe.utility.os_ext as os_ext from reframe.core.exceptions import (EnvironError, ConfigError, ReframeError, ReframeFatalError, format_exception, SystemAutodetectionError) -from reframe.frontend.argparse import ArgumentParser from reframe.frontend.executors import Runner from reframe.frontend.executors.policies import (SerialExecutionPolicy, AsynchronousExecutionPolicy) from reframe.frontend.loader import RegressionCheckLoader from reframe.frontend.printer import PrettyPrinter + def format_check(check, detailed): lines = [' * %s (found in %s)' % (check.name, inspect.getfile(type(check)))] @@ -48,7 +49,7 @@ def list_checks(checks, printer, detailed=False): def main(): # Setup command line options - argparser = ArgumentParser() + argparser = argparse.ArgumentParser() output_options = argparser.add_argument_group( 'Options controlling regression directories') locate_options = argparser.add_argument_group( @@ -255,6 +256,9 @@ def main(): sys.stderr.write('could not configure logging: %s\n' % e) sys.exit(1) + # Set colors in logger + logging.getlogger().colorize = options.colorize + # Setup printer printer = PrettyPrinter() printer.colorize = options.colorize @@ -377,7 +381,7 @@ def main(): prefix=reframe.INSTALL_PREFIX, recurse=settings.checks_path_recurse) - printer.log_config(options) + printer.debug(argparse.format_options(options)) # Print command line printer.info('Command line: %s' % ' '.join(sys.argv)) @@ -452,7 +456,7 @@ def main(): rt.modules_system.load_module(m, force=True) raise EnvironError("test") except EnvironError as e: - printer.warning("could not load module '%s' correctly: " + printer.warning("could not load module '%s' correctly: " "Skipping..." % m) printer.debug(str(e)) diff --git a/reframe/frontend/executors/__init__.py b/reframe/frontend/executors/__init__.py index 1af804be0b..79fdb7361d 100644 --- a/reframe/frontend/executors/__init__.py +++ b/reframe/frontend/executors/__init__.py @@ -172,7 +172,7 @@ def runall(self, checks): self._printer.separator('short double line', 'Running %d check(s)' % len(checks)) self._printer.timestamp('Started on', 'short double line') - self._printer.info() + self._printer.info('') self._runall(checks) if self._max_retries: self._retry_failed(checks) diff --git a/reframe/frontend/printer.py b/reframe/frontend/printer.py index 0c2bed3689..61b7ff4e30 100644 --- a/reframe/frontend/printer.py +++ b/reframe/frontend/printer.py @@ -1,65 +1,24 @@ -import abc import datetime import sys -import reframe.core.debug as debug import reframe.core.logging as logging - - -class Colorizer(abc.ABC): - def __repr__(self): - return debug.repr(self) - - @abc.abstractmethod - def colorize(string, foreground, background): - """Colorize a string. - - Keyword arguments: - string -- the string to be colorized - foreground -- the foreground color - background -- the background color - """ - - -class AnsiColorizer(Colorizer): - escape_seq = '\033' - reset_term = '[0m' - - # Escape sequences for fore/background colors - fgcolor = '[3' - bgcolor = '[4' - - # color values - black = '0m' - red = '1m' - green = '2m' - yellow = '3m' - blue = '4m' - magenta = '5m' - cyan = '6m' - white = '7m' - default = '9m' - - def colorize(string, foreground, background=None): - return (AnsiColorizer.escape_seq + - AnsiColorizer.fgcolor + foreground + string + - AnsiColorizer.escape_seq + AnsiColorizer.reset_term) +from reframe.utility import AnsiColorizer class PrettyPrinter: """Pretty printing facility for the framework. - Final printing is delegated to an internal logger, which is responsible for - printing both to standard output and in a special output file.""" + It takes care of formatting the progress output and adds some more + cosmetics to specific levels of messages, such as warnings and errors. + + The actual printing is delegated to an internal logger, which is + responsible for printing. + """ def __init__(self): self.colorize = True self.line_width = 78 self.status_width = 10 - self._logger = logging.getlogger() - - def __repr__(self): - return debug.repr(self) def separator(self, linestyle, msg=''): if linestyle == 'short double line': @@ -88,7 +47,7 @@ def status(self, status, message='', just=None, level=logging.INFO): else: status = AnsiColorizer.colorize(status, AnsiColorizer.green) - self._logger.log(level, '[ %s ] %s' % (status, message)) + logging.getlogger().log(level, '[ %s ] %s' % (status, message)) def result(self, check, partition, environ, success): if success: @@ -107,24 +66,14 @@ def timestamp(self, msg='', separator=None): else: self.info(msg) - def info(self, msg=''): - self._logger.info(msg) - - def debug(self, msg=''): - self._logger.debug(msg) + def __getattr__(self, attr): + # delegate all other attribute lookup to the underlying logger + return getattr(logging.getlogger(), attr) def warning(self, msg): - msg = AnsiColorizer.colorize('%s: %s' % (sys.argv[0], msg), - AnsiColorizer.yellow) - self._logger.warning(msg) + msg = '%s: %s' % (sys.argv[0], msg) + logging.getlogger().warning(msg) def error(self, msg): - msg = AnsiColorizer.colorize('%s: %s' % (sys.argv[0], msg), - AnsiColorizer.red) - self._logger.error(msg) - - def log_config(self, options): - opt_list = [' %s=%s' % (attr, val) - for attr, val in sorted(options.__dict__.items())] - - self._logger.debug('configuration\n%s' % '\n'.join(opt_list)) + msg = '%s: %s' % (sys.argv[0], msg) + logging.getlogger().error(msg) diff --git a/reframe/utility/__init__.py b/reframe/utility/__init__.py index 9ec8c05eab..7c7356eb35 100644 --- a/reframe/utility/__init__.py +++ b/reframe/utility/__init__.py @@ -1,3 +1,4 @@ +import abc import collections import importlib import importlib.util @@ -310,3 +311,47 @@ def __eq__(self, *args, **kwargs): def __ne__(self, *args, **kwargs): return self.__mapping.__ne__(*args, **kwargs) + + +class Colorizer(abc.ABC): + """Base class for colorizing strings on the terminal.""" + + def __repr__(self): + return debug.repr(self) + + @abc.abstractmethod + def colorize(string, foreground, background): + """Colorize a string. + + Keyword arguments: + string -- the string to be colorized + foreground -- the foreground color + background -- the background color + """ + + +class AnsiColorizer(Colorizer): + """Class for colorizing strings using ANSI meta-characters.""" + + escape_seq = '\033' + reset_term = '[0m' + + # Escape sequences for fore/background colors + fgcolor = '[3' + bgcolor = '[4' + + # color values + black = '0m' + red = '1m' + green = '2m' + yellow = '3m' + blue = '4m' + magenta = '5m' + cyan = '6m' + white = '7m' + default = '9m' + + def colorize(string, foreground, background=None): + return (AnsiColorizer.escape_seq + + AnsiColorizer.fgcolor + foreground + string + + AnsiColorizer.escape_seq + AnsiColorizer.reset_term) From adf1e00cd554d2beff3010007f486b1dcf37cb5b Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Mon, 21 Jan 2019 09:02:36 +0100 Subject: [PATCH 2/3] Prefix errors and warnings with progname in logger --- reframe/core/logging.py | 2 ++ reframe/frontend/printer.py | 8 -------- unittests/test_logging.py | 9 ++++----- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/reframe/core/logging.py b/reframe/core/logging.py index a7ef8739ff..bbc3fbd7ff 100644 --- a/reframe/core/logging.py +++ b/reframe/core/logging.py @@ -418,12 +418,14 @@ def verbose(self, message, *args, **kwargs): self.log(VERBOSE, message, *args, **kwargs) def warning(self, message, *args, **kwargs): + message = '%s: %s' % (sys.argv[0], message) if self.colorize: message = AnsiColorizer.colorize(message, AnsiColorizer.yellow) super().warning(message, *args, **kwargs) def error(self, message, *args, **kwargs): + message = '%s: %s' % (sys.argv[0], message) if self.colorize: message = AnsiColorizer.colorize(message, AnsiColorizer.red) diff --git a/reframe/frontend/printer.py b/reframe/frontend/printer.py index 61b7ff4e30..3ae459f26f 100644 --- a/reframe/frontend/printer.py +++ b/reframe/frontend/printer.py @@ -69,11 +69,3 @@ def timestamp(self, msg='', separator=None): def __getattr__(self, attr): # delegate all other attribute lookup to the underlying logger return getattr(logging.getlogger(), attr) - - def warning(self, msg): - msg = '%s: %s' % (sys.argv[0], msg) - logging.getlogger().warning(msg) - - def error(self, msg): - msg = '%s: %s' % (sys.argv[0], msg) - logging.getlogger().error(msg) diff --git a/unittests/test_logging.py b/unittests/test_logging.py index 3ef799a011..513856caa8 100644 --- a/unittests/test_logging.py +++ b/unittests/test_logging.py @@ -358,11 +358,10 @@ def test_logging_context_check(self): rlog.getlogger().error('error from context') rlog.getlogger().error('error outside context') - - self.assertTrue( - self.found_in_logfile('random_check: error from context')) - self.assertTrue( - self.found_in_logfile('reframe: error outside context')) + self.assertTrue(self.found_in_logfile( + 'random_check: %s: error from context' % sys.argv[0])) + self.assertTrue(self.found_in_logfile( + 'reframe: %s: error outside context' % sys.argv[0])) def test_logging_context_error(self): rlog.configure_logging(self.logging_config) From d0feba8fb329b6866eec841b5d76918fa0341223 Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Mon, 21 Jan 2019 17:31:34 +0100 Subject: [PATCH 3/3] Refactor code for coloring TTY output --- reframe/core/logging.py | 6 +-- reframe/frontend/printer.py | 8 ++-- reframe/utility/__init__.py | 44 ------------------- reframe/utility/color.py | 85 +++++++++++++++++++++++++++++++++++++ unittests/test_color.py | 27 ++++++++++++ 5 files changed, 119 insertions(+), 51 deletions(-) create mode 100644 reframe/utility/color.py create mode 100644 unittests/test_color.py diff --git a/reframe/core/logging.py b/reframe/core/logging.py index bbc3fbd7ff..697502435f 100644 --- a/reframe/core/logging.py +++ b/reframe/core/logging.py @@ -11,10 +11,10 @@ from datetime import datetime import reframe +import reframe.utility.color as color import reframe.core.debug as debug import reframe.utility.os_ext as os_ext from reframe.core.exceptions import ConfigError, LoggingError -from reframe.utility import AnsiColorizer # Global configuration options for logging @@ -420,14 +420,14 @@ def verbose(self, message, *args, **kwargs): def warning(self, message, *args, **kwargs): message = '%s: %s' % (sys.argv[0], message) if self.colorize: - message = AnsiColorizer.colorize(message, AnsiColorizer.yellow) + message = color.colorize(message, color.YELLOW) super().warning(message, *args, **kwargs) def error(self, message, *args, **kwargs): message = '%s: %s' % (sys.argv[0], message) if self.colorize: - message = AnsiColorizer.colorize(message, AnsiColorizer.red) + message = color.colorize(message, color.RED) super().error(message, *args, **kwargs) diff --git a/reframe/frontend/printer.py b/reframe/frontend/printer.py index 3ae459f26f..ab1f340f02 100644 --- a/reframe/frontend/printer.py +++ b/reframe/frontend/printer.py @@ -2,7 +2,7 @@ import sys import reframe.core.logging as logging -from reframe.utility import AnsiColorizer +import reframe.utility.color as color class PrettyPrinter: @@ -41,11 +41,11 @@ def status(self, status, message='', just=None, level=logging.INFO): if self.colorize: status_stripped = status.strip().lower() if status_stripped == 'skip': - status = AnsiColorizer.colorize(status, AnsiColorizer.yellow) + status = color.colorize(status, color.YELLOW) elif status_stripped in ['fail', 'failed']: - status = AnsiColorizer.colorize(status, AnsiColorizer.red) + status = color.colorize(status, color.RED) else: - status = AnsiColorizer.colorize(status, AnsiColorizer.green) + status = color.colorize(status, color.GREEN) logging.getlogger().log(level, '[ %s ] %s' % (status, message)) diff --git a/reframe/utility/__init__.py b/reframe/utility/__init__.py index 7c7356eb35..c1da7d3205 100644 --- a/reframe/utility/__init__.py +++ b/reframe/utility/__init__.py @@ -311,47 +311,3 @@ def __eq__(self, *args, **kwargs): def __ne__(self, *args, **kwargs): return self.__mapping.__ne__(*args, **kwargs) - - -class Colorizer(abc.ABC): - """Base class for colorizing strings on the terminal.""" - - def __repr__(self): - return debug.repr(self) - - @abc.abstractmethod - def colorize(string, foreground, background): - """Colorize a string. - - Keyword arguments: - string -- the string to be colorized - foreground -- the foreground color - background -- the background color - """ - - -class AnsiColorizer(Colorizer): - """Class for colorizing strings using ANSI meta-characters.""" - - escape_seq = '\033' - reset_term = '[0m' - - # Escape sequences for fore/background colors - fgcolor = '[3' - bgcolor = '[4' - - # color values - black = '0m' - red = '1m' - green = '2m' - yellow = '3m' - blue = '4m' - magenta = '5m' - cyan = '6m' - white = '7m' - default = '9m' - - def colorize(string, foreground, background=None): - return (AnsiColorizer.escape_seq + - AnsiColorizer.fgcolor + foreground + string + - AnsiColorizer.escape_seq + AnsiColorizer.reset_term) diff --git a/reframe/utility/color.py b/reframe/utility/color.py new file mode 100644 index 0000000000..2ebff6778a --- /dev/null +++ b/reframe/utility/color.py @@ -0,0 +1,85 @@ +class ColorRGB: + def __init__(self, r, g, b): + self.__check_rgb(r) + self.__check_rgb(g) + self.__check_rgb(b) + self.__r = r + self.__g = g + self.__b = b + + def __check_rgb(self, x): + if (x < 0) or x > 255: + raise ValueError('RGB color code must be in [0,255]') + + @property + def r(self): + return self.__r + + @property + def g(self): + return self.__g + + @property + def b(self): + return self.__b + + def __repr__(self): + return 'ColorRGB(%s, %s, %s)' % (self.__r, self.__g, self.__b) + + +# Predefined colors +BLACK = ColorRGB(0, 0, 0) +RED = ColorRGB(255, 0, 0) +GREEN = ColorRGB(0, 255, 0) +YELLOW = ColorRGB(255, 255, 0) +BLUE = ColorRGB(0, 0, 255) +MAGENTA = ColorRGB(255, 0, 255) +CYAN = ColorRGB(0, 255, 255) +WHITE = ColorRGB(255, 255, 255) + + +class _AnsiPalette: + """Class for colorizing strings using ANSI meta-characters.""" + + escape_seq = '\033' + reset_term = '[0m' + + # Escape sequences for fore/background colors + fgcolor = '[3' + bgcolor = '[4' + + # color values + colors = { + BLACK: '0m', + RED: '1m', + GREEN: '2m', + YELLOW: '3m', + BLUE: '4m', + MAGENTA: '5m', + CYAN: '6m', + WHITE: '7m' + } + + def colorize(string, foreground): + try: + foreground = _AnsiPalette.colors[foreground] + except KeyError: + raise ValueError('could not find an ANSI representation ' + 'for color: %s' % foreground) from None + + return (_AnsiPalette.escape_seq + + _AnsiPalette.fgcolor + foreground + string + + _AnsiPalette.escape_seq + _AnsiPalette.reset_term) + + +def colorize(string, foreground, *, palette='ANSI'): + """Colorize a string. + + :arg string: The string to be colorized. + :arg foreground: The foreground color. + :arg palette: The palette to get colors from. + """ + if palette != 'ANSI': + raise ValueError('unknown color palette: %s' % palette) + + return _AnsiPalette.colorize(string, foreground) diff --git a/unittests/test_color.py b/unittests/test_color.py new file mode 100644 index 0000000000..7dcde74d1e --- /dev/null +++ b/unittests/test_color.py @@ -0,0 +1,27 @@ +import unittest + +import reframe.utility.color as color + + +class TestColors(unittest.TestCase): + def test_color_rgb(self): + c = color.ColorRGB(128, 0, 34) + self.assertEqual(128, c.r) + self.assertEqual(0, c.g) + self.assertEqual(34, c.b) + + self.assertRaises(ValueError, color.ColorRGB, -1, 0, 34) + self.assertRaises(ValueError, color.ColorRGB, 0, -1, 34) + self.assertRaises(ValueError, color.ColorRGB, 0, 28, -1) + + def test_colorize(self): + s = color.colorize('hello', color.RED, palette='ANSI') + self.assertIn('\033', s) + self.assertIn('[3', s) + self.assertIn('1m', s) + + with self.assertRaises(ValueError): + color.colorize('hello', color.RED, palette='FOO') + + with self.assertRaises(ValueError): + color.colorize('hello', color.ColorRGB(128, 0, 34), palette='ANSI')